Browse Source

Merge branch 'main' of https://github.com/ionite34/StabilityMatrix

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

14
CHANGELOG.md

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

12
StabilityMatrix.Avalonia/Animations/BaseTransitionInfo.cs

@ -0,0 +1,12 @@
using System;
using FluentAvalonia.UI.Media.Animation;
namespace StabilityMatrix.Avalonia.Animations;
public abstract class BaseTransitionInfo : NavigationTransitionInfo
{
/// <summary>
/// The duration of the animation at 1x animation scale
/// </summary>
public abstract TimeSpan Duration { get; set; }
}

59
StabilityMatrix.Avalonia/Animations/BetterDrillInNavigationTransition.cs

@ -0,0 +1,59 @@
using System;
using System.Threading;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
namespace StabilityMatrix.Avalonia.Animations;
public class BetterDrillInNavigationTransition : BaseTransitionInfo
{
/// <summary>
/// Gets or sets whether the animation should drill in (false) or drill out (true)
/// </summary>
public bool IsReversed { get; set; } = false; //Zoom out if true
public override TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(400);
public override async void RunAnimation(Animatable ctrl, CancellationToken cancellationToken)
{
var animation = new Animation
{
Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0),
Children =
{
new KeyFrame
{
Setters =
{
new Setter(Visual.OpacityProperty, 0.0),
new Setter(ScaleTransform.ScaleXProperty, IsReversed ? 1.5 : 0.0),
new Setter(ScaleTransform.ScaleYProperty, IsReversed ? 1.5 : 0.0)
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter(Visual.OpacityProperty, 1.0),
new Setter(ScaleTransform.ScaleXProperty, IsReversed ? 1.0 : 1.0),
new Setter(ScaleTransform.ScaleYProperty, IsReversed ? 1.0 : 1.0)
},
Cue = new Cue(1d)
}
},
Duration = Duration,
FillMode = FillMode.Forward
};
await animation.RunAsync(ctrl, cancellationToken);
if (ctrl is Visual visualCtrl)
{
visualCtrl.Opacity = 1;
}
}
}

66
StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs

@ -0,0 +1,66 @@
using System;
using System.Threading;
using AsyncAwaitBestPractices;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
using FluentAvalonia.UI.Media.Animation;
namespace StabilityMatrix.Avalonia.Animations;
public class BetterEntranceNavigationTransition : BaseTransitionInfo
{
public override TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(500);
/// <summary>
/// Gets or sets the Horizontal Offset used when animating
/// </summary>
public double FromHorizontalOffset { get; set; } = 0;
/// <summary>
/// Gets or sets the Vertical Offset used when animating
/// </summary>
public double FromVerticalOffset { get; set; } = 100;
public override async void RunAnimation(Animatable ctrl, CancellationToken cancellationToken)
{
var animation = new Animation
{
Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0),
Children =
{
new KeyFrame
{
Setters =
{
new Setter(Visual.OpacityProperty, 0.0),
new Setter(TranslateTransform.XProperty,FromHorizontalOffset),
new Setter(TranslateTransform.YProperty, FromVerticalOffset)
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter(Visual.OpacityProperty, 1d),
new Setter(TranslateTransform.XProperty,0.0),
new Setter(TranslateTransform.YProperty, 0.0)
},
Cue = new Cue(1d)
}
},
Duration = Duration,
FillMode = FillMode.Forward
};
await animation.RunAsync(ctrl, cancellationToken);
if (ctrl is Visual visualCtrl)
{
visualCtrl.Opacity = 1;
}
}
}

96
StabilityMatrix.Avalonia/Animations/BetterSlideNavigationTransition.cs

@ -0,0 +1,96 @@
using System;
using System.Threading;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
using FluentAvalonia.UI.Media.Animation;
namespace StabilityMatrix.Avalonia.Animations;
public class BetterSlideNavigationTransition : BaseTransitionInfo
{
public override TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(167);
/// <summary>
/// Gets or sets the type of animation effect to play during the slide transition.
/// </summary>
public SlideNavigationTransitionEffect Effect { get; set; } = SlideNavigationTransitionEffect.FromRight;
/// <summary>
/// Gets or sets the HorizontalOffset used when animating from the Left or Right
/// </summary>
public double FromHorizontalOffset { get; set; } = 56;
/// <summary>
/// Gets or sets the VerticalOffset used when animating from the Top or Bottom
/// </summary>
public double FromVerticalOffset { get; set; } = 56;
public override async void RunAnimation(Animatable ctrl, CancellationToken cancellationToken)
{
double length = 0;
bool isVertical = false;
switch (Effect)
{
case SlideNavigationTransitionEffect.FromLeft:
length = -FromHorizontalOffset;
break;
case SlideNavigationTransitionEffect.FromRight:
length = FromHorizontalOffset;
break;
case SlideNavigationTransitionEffect.FromTop:
length = -FromVerticalOffset;
isVertical = true;
break;
case SlideNavigationTransitionEffect.FromBottom:
length = FromVerticalOffset;
isVertical = true;
break;
}
var animation = new Animation
{
Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0),
Children =
{
new KeyFrame
{
Setters =
{
new Setter(isVertical ? TranslateTransform.YProperty : TranslateTransform.XProperty, length),
new Setter(Visual.OpacityProperty, 0d)
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters=
{
new Setter(Visual.OpacityProperty, 1d)
},
Cue = new Cue(0.05d)
},
new KeyFrame
{
Setters =
{
new Setter(Visual.OpacityProperty, 1d),
new Setter(isVertical ? TranslateTransform.YProperty : TranslateTransform.XProperty, 0.0)
},
Cue = new Cue(1d)
}
},
Duration = Duration,
FillMode = FillMode.Forward
};
await animation.RunAsync(ctrl, cancellationToken);
if (ctrl is Visual visual)
{
visual.Opacity = 1;
}
}
}

18
StabilityMatrix.Avalonia/App.axaml.cs

@ -37,7 +37,10 @@ using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
@ -54,6 +57,8 @@ using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
using Application = Avalonia.Application; using Application = Avalonia.Application;
using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile;
using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
using LogLevel = Microsoft.Extensions.Logging.LogLevel; using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace StabilityMatrix.Avalonia; namespace StabilityMatrix.Avalonia;
@ -141,11 +146,9 @@ public sealed class App : Application
if (DesktopLifetime is null) return; if (DesktopLifetime is null) return;
var mainViewModel = Services.GetRequiredService<MainWindowViewModel>(); var mainViewModel = Services.GetRequiredService<MainWindowViewModel>();
var notificationService = Services.GetRequiredService<INotificationService>();
var mainWindow = Services.GetRequiredService<MainWindow>(); var mainWindow = Services.GetRequiredService<MainWindow>();
mainWindow.DataContext = mainViewModel; mainWindow.DataContext = mainViewModel;
mainWindow.NotificationService = notificationService;
mainWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ? mainWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ?
ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome; ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome;
@ -200,7 +203,6 @@ public sealed class App : Application
services.AddSingleton<PackageManagerViewModel>() services.AddSingleton<PackageManagerViewModel>()
.AddSingleton<SettingsViewModel>() .AddSingleton<SettingsViewModel>()
.AddSingleton<CheckpointBrowserViewModel>() .AddSingleton<CheckpointBrowserViewModel>()
.AddSingleton<CheckpointBrowserCardViewModel>()
.AddSingleton<CheckpointsPageViewModel>() .AddSingleton<CheckpointsPageViewModel>()
.AddSingleton<LaunchPageViewModel>() .AddSingleton<LaunchPageViewModel>()
.AddSingleton<ProgressManagerViewModel>(); .AddSingleton<ProgressManagerViewModel>();
@ -242,8 +244,11 @@ public sealed class App : Application
services.AddSingleton<UpdateViewModel>(); services.AddSingleton<UpdateViewModel>();
// Other transients (usually sub view models) // Other transients (usually sub view models)
services.AddTransient<CheckpointFolder>(); services.AddTransient<CheckpointFolder>()
services.AddTransient<CheckpointFile>(); .AddTransient<CheckpointFile>()
.AddTransient<CheckpointBrowserCardViewModel>();
services.AddTransient<PackageCardViewModel>();
// Global progress // Global progress
services.AddSingleton<ProgressManagerViewModel>(); services.AddSingleton<ProgressManagerViewModel>();
@ -260,8 +265,10 @@ public sealed class App : Application
.Register(provider.GetRequiredService<SelectDataDirectoryViewModel>) .Register(provider.GetRequiredService<SelectDataDirectoryViewModel>)
.Register(provider.GetRequiredService<LaunchOptionsViewModel>) .Register(provider.GetRequiredService<LaunchOptionsViewModel>)
.Register(provider.GetRequiredService<UpdateViewModel>) .Register(provider.GetRequiredService<UpdateViewModel>)
.Register(provider.GetRequiredService<CheckpointBrowserCardViewModel>)
.Register(provider.GetRequiredService<CheckpointFolder>) .Register(provider.GetRequiredService<CheckpointFolder>)
.Register(provider.GetRequiredService<CheckpointFile>) .Register(provider.GetRequiredService<CheckpointFile>)
.Register(provider.GetRequiredService<PackageCardViewModel>)
.Register(provider.GetRequiredService<RefreshBadgeViewModel>) .Register(provider.GetRequiredService<RefreshBadgeViewModel>)
.Register(provider.GetRequiredService<ExceptionViewModel>) .Register(provider.GetRequiredService<ExceptionViewModel>)
.Register(provider.GetRequiredService<EnvVarsViewModel>) .Register(provider.GetRequiredService<EnvVarsViewModel>)
@ -324,6 +331,7 @@ public sealed class App : Application
services.AddSingleton<INotificationService, NotificationService>(); services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IPyRunner, PyRunner>(); services.AddSingleton<IPyRunner, PyRunner>();
services.AddSingleton<IUpdateHelper, UpdateHelper>(); services.AddSingleton<IUpdateHelper, UpdateHelper>();
services.AddSingleton<INavigationService, NavigationService>();
// Rich presence // Rich presence
services.AddSingleton<IDiscordRichPresenceService, DiscordRichPresenceService>(); services.AddSingleton<IDiscordRichPresenceService, DiscordRichPresenceService>();

2
StabilityMatrix.Avalonia/Controls/AppWindowBase.cs

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

2
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

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

2
StabilityMatrix.Avalonia/Controls/UserControlBase.cs

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

79
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,12 +4,16 @@ using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -21,6 +25,8 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile;
using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
namespace StabilityMatrix.Avalonia.DesignData; namespace StabilityMatrix.Avalonia.DesignData;
@ -35,7 +41,8 @@ public static class DesignData
// or else Avalonia analyzers won't work. // or else Avalonia analyzers won't work.
public static void Initialize() public static void Initialize()
{ {
if (isInitialized) throw new InvalidOperationException("DesignData is already initialized."); if (isInitialized)
throw new InvalidOperationException("DesignData is already initialized.");
var services = new ServiceCollection(); var services = new ServiceCollection();
@ -50,6 +57,7 @@ public static class DesignData
{ {
Id = activePackageId, Id = activePackageId,
DisplayName = "My Installed Package", DisplayName = "My Installed Package",
DisplayVersion = "v1.0.0",
PackageName = "stable-diffusion-webui", PackageName = "stable-diffusion-webui",
PackageVersion = "v1.0.0", PackageVersion = "v1.0.0",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
@ -59,8 +67,8 @@ public static class DesignData
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
DisplayName = "Dank Diffusion", DisplayName = "Dank Diffusion",
PackageName = "dank-diffusion", PackageName = "ComfyUI",
PackageVersion = "v2.0.0", DisplayVersion = "main@ab73d4a",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now LastUpdateCheck = DateTimeOffset.Now
} }
@ -71,6 +79,7 @@ public static class DesignData
// General services // General services
services.AddLogging() services.AddLogging()
.AddSingleton<INavigationService, NavigationService>()
.AddSingleton<IPackageFactory, PackageFactory>() .AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<IUpdateHelper, UpdateHelper>() .AddSingleton<IUpdateHelper, UpdateHelper>()
.AddSingleton<ModelFinder>() .AddSingleton<ModelFinder>()
@ -120,13 +129,13 @@ public static class DesignData
Type = LaunchOptionType.String, Type = LaunchOptionType.String,
Description = "The host name for the Web UI", Description = "The host name for the Web UI",
DefaultValue = "localhost", DefaultValue = "localhost",
Options = { "--host" } Options = {"--host"}
}), }),
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition LaunchOptionCard.FromDefinition(new LaunchOptionDefinition
{ {
Name = "API", Name = "API",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
Options = { "--api" } Options = {"--api"}
}) })
}; };
LaunchOptionsViewModel.UpdateFilterCards(); LaunchOptionsViewModel.UpdateFilterCards();
@ -204,24 +213,29 @@ public static class DesignData
CheckpointBrowserViewModel.ModelCards = new CheckpointBrowserViewModel.ModelCards = new
ObservableCollection<CheckpointBrowserCardViewModel> ObservableCollection<CheckpointBrowserCardViewModel>
{ {
new(new CivitModel dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = new CivitModel
{ {
Name = "BB95 Furry Mix", Name = "BB95 Furry Mix",
Description = "A furry mix of BB95", Description = "A furry mix of BB95",
}, downloadService, settingsManager, };
dialogFactory, notificationService) })
}; };
ProgressManagerViewModel.ProgressItems = new ObservableCollection<ProgressItemViewModel> ProgressManagerViewModel.ProgressItems = new ObservableCollection<ProgressItemViewModel>
{ {
new(new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))), new(new ProgressItem(Guid.NewGuid(), "Test File.exe",
new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu", new ProgressReport(0.25f, "Extracting..."))) new ProgressReport(0.5f, "Downloading..."))),
new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu",
new ProgressReport(0.25f, "Extracting...")))
}; };
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>(); UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.UpdateText = UpdateViewModel.UpdateText =
$"Stability Matrix v2.0.1 is now available! You currently have v2.0.0. Would you like to update now?"; $"Stability Matrix v2.0.1 is now available! You currently have v2.0.0. Would you like to update now?";
UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature"; UpdateViewModel.ReleaseNotes =
"## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature";
isInitialized = true; isInitialized = true;
} }
@ -232,20 +246,39 @@ public static class DesignData
public static ServiceManager<ViewModelBase> DialogFactory => public static ServiceManager<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>(); Services.GetRequiredService<ServiceManager<ViewModelBase>>();
public static MainWindowViewModel MainWindowViewModel => public static MainWindowViewModel MainWindowViewModel =>
Services.GetRequiredService<MainWindowViewModel>(); Services.GetRequiredService<MainWindowViewModel>();
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel => public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
Services.GetRequiredService<FirstLaunchSetupViewModel>(); Services.GetRequiredService<FirstLaunchSetupViewModel>();
public static LaunchPageViewModel LaunchPageViewModel => public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<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 => public static CheckpointsPageViewModel CheckpointsPageViewModel =>
Services.GetRequiredService<CheckpointsPageViewModel>(); Services.GetRequiredService<CheckpointsPageViewModel>();
public static SettingsViewModel SettingsViewModel => public static SettingsViewModel SettingsViewModel =>
Services.GetRequiredService<SettingsViewModel>(); Services.GetRequiredService<SettingsViewModel>();
public static CheckpointBrowserViewModel CheckpointBrowserViewModel => public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService<CheckpointBrowserViewModel>(); Services.GetRequiredService<CheckpointBrowserViewModel>();
public static SelectModelVersionViewModel SelectModelVersionViewModel => public static SelectModelVersionViewModel SelectModelVersionViewModel =>
DialogFactory.Get<SelectModelVersionViewModel>(vm => DialogFactory.Get<SelectModelVersionViewModel>(vm =>
{ {
@ -256,6 +289,7 @@ public static class DesignData
{ {
Name = "BB95 Furry Mix", Name = "BB95 Furry Mix",
Description = "v1.0.0", Description = "v1.0.0",
BaseModel = "SD 1.5",
Files = new List<CivitFile> Files = new List<CivitFile>
{ {
new() new()
@ -291,15 +325,19 @@ public static class DesignData
new ModelVersionViewModel(new HashSet<string> {"ABCD"}, sampleCivitVersions[0]); new ModelVersionViewModel(new HashSet<string> {"ABCD"}, sampleCivitVersions[0]);
// Sample data for dialogs // Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel>{sampleViewModel}; vm.Versions = new List<ModelVersionViewModel> {sampleViewModel};
vm.SelectedVersionViewModel = sampleViewModel; vm.SelectedVersionViewModel = sampleViewModel;
}); });
public static OneClickInstallViewModel OneClickInstallViewModel => public static OneClickInstallViewModel OneClickInstallViewModel =>
Services.GetRequiredService<OneClickInstallViewModel>(); Services.GetRequiredService<OneClickInstallViewModel>();
public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel => public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel =>
Services.GetRequiredService<SelectDataDirectoryViewModel>(); Services.GetRequiredService<SelectDataDirectoryViewModel>();
public static ProgressManagerViewModel ProgressManagerViewModel => public static ProgressManagerViewModel ProgressManagerViewModel =>
Services.GetRequiredService<ProgressManagerViewModel>(); Services.GetRequiredService<ProgressManagerViewModel>();
public static ExceptionViewModel ExceptionViewModel => public static ExceptionViewModel ExceptionViewModel =>
DialogFactory.Get<ExceptionViewModel>(viewModel => DialogFactory.Get<ExceptionViewModel>(viewModel =>
{ {
@ -321,13 +359,14 @@ public static class DesignData
} }
}); });
public static EnvVarsViewModel EnvVarsViewModel => DialogFactory.Get<EnvVarsViewModel>(viewModel => public static EnvVarsViewModel EnvVarsViewModel => DialogFactory.Get<EnvVarsViewModel>(
{ viewModel =>
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>
{ {
new("UWU", "TRUE"), viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>
}; {
}); new("UWU", "TRUE"),
};
});
public static RefreshBadgeViewModel RefreshBadgeViewModel => new() public static RefreshBadgeViewModel RefreshBadgeViewModel => new()
{ {

1
StabilityMatrix.Avalonia/DesignData/MockLaunchPageViewModel.cs

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

7
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

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

1
StabilityMatrix.Avalonia/Program.cs

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

23
StabilityMatrix.Avalonia/Services/INavigationService.cs

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

99
StabilityMatrix.Avalonia/Services/NavigationService.cs

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Navigation;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Services;
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.");
}
if (App.Services.GetService(typeof(ISettingsManager)) is ISettingsManager settingsManager)
{
// Handle animation scale
switch (transitionInfo)
{
// If the transition info is null or animation scale is 0, suppress the transition
case null:
case BaseTransitionInfo when settingsManager.Settings.AnimationScale == 0f:
transitionInfo = new SuppressNavigationTransitionInfo();
break;
case BaseTransitionInfo baseTransitionInfo:
baseTransitionInfo.Duration *= settingsManager.Settings.AnimationScale;
break;
}
}
_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.");
}
if (App.Services.GetService(typeof(ISettingsManager)) is ISettingsManager settingsManager)
{
// Handle animation scale
switch (transitionInfo)
{
// If the transition info is null or animation scale is 0, suppress the transition
case null:
case BaseTransitionInfo when settingsManager.Settings.AnimationScale == 0f:
transitionInfo = new SuppressNavigationTransitionInfo();
break;
case BaseTransitionInfo baseTransitionInfo:
baseTransitionInfo.Duration *= settingsManager.Settings.AnimationScale;
break;
}
}
_frame.NavigateFromObject(viewModel,
new FrameNavigationOptions
{
IsNavigationStackEnabled = true,
TransitionInfoOverride = transitionInfo ?? new SuppressNavigationTransitionInfo()
});
}
}

68
StabilityMatrix.Avalonia/ViewLocator.cs

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

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

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

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

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

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

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

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

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

37
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

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

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

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

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

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

15
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

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

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

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

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

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

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

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

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

@ -38,7 +38,6 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
private readonly IPyRunner pyRunner; private readonly IPyRunner pyRunner;
private readonly IDownloadService downloadService; private readonly IDownloadService downloadService;
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly ISharedFolders sharedFolders;
private readonly IPrerequisiteHelper prerequisiteHelper; private readonly IPrerequisiteHelper prerequisiteHelper;
[ObservableProperty] private BasePackage selectedPackage; [ObservableProperty] private BasePackage selectedPackage;
@ -75,21 +74,19 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
[ObservableProperty] private string? installName; [ObservableProperty] private string? installName;
public ProgressViewModel InstallProgress { get; } = new(); public Base.ProgressViewModel InstallProgress { get; } = new();
public InstallerViewModel( public InstallerViewModel(
ISettingsManager settingsManager, ISettingsManager settingsManager,
IPackageFactory packageFactory, IPackageFactory packageFactory,
IPyRunner pyRunner, IPyRunner pyRunner,
IDownloadService downloadService, INotificationService notificationService, IDownloadService downloadService, INotificationService notificationService,
ISharedFolders sharedFolders,
IPrerequisiteHelper prerequisiteHelper) IPrerequisiteHelper prerequisiteHelper)
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.pyRunner = pyRunner; this.pyRunner = pyRunner;
this.downloadService = downloadService; this.downloadService = downloadService;
this.notificationService = notificationService; this.notificationService = notificationService;
this.sharedFolders = sharedFolders;
this.prerequisiteHelper = prerequisiteHelper; this.prerequisiteHelper = prerequisiteHelper;
// AvailablePackages and SelectedPackage // AvailablePackages and SelectedPackage

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

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

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

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

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

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

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

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

1
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

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

5
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

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

17
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

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

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

@ -0,0 +1,309 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using NLog;
using Polly;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Extensions;
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 : 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 BetterDrillInNavigationTransition());
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.Warn("Could not find package {SelectedPackagePackageName}",
Package.PackageName);
notificationService.Show("Invalid Package type",
$"Package {Package.PackageName.ToRepr()} is not a valid package type",
NotificationType.Error);
return;
}
Text = $"Updating {Package.DisplayName}";
IsIndeterminate = true;
var progressId = Guid.NewGuid();
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName,
new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update)));
try
{
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);
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName, progress));
});
var updateResult = await basePackage.Update(Package, progress);
settingsManager.UpdatePackageVersionNumber(Package.Id, updateResult);
notificationService.Show("Update complete",
$"{Package.DisplayName} has been updated to the latest version.",
NotificationType.Success);
await using (settingsManager.BeginTransaction())
{
Package.UpdateAvailable = false;
}
IsUpdateAvailable = false;
InstalledVersion = Package.DisplayVersion ?? "Unknown";
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName,
new ProgressReport(1f, "Update complete", type: ProgressType.Update)));
}
catch (Exception e)
{
logger.Error(e, "Error Updating Package ({PackageName})", basePackage.Name);
notificationService.ShowPersistent($"Error Updating {Package.DisplayName}", e.Message, NotificationType.Error);
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName,
new ProgressReport(0f, "Update failed", type: ProgressType.Update), Failed: true));
}
finally
{
IsIndeterminate = false;
Value = 0;
Text = "";
}
}
public async Task OpenFolder()
{
if (string.IsNullOrWhiteSpace(Package?.FullPath))
return;
await ProcessRunner.OpenFolderBrowser(Package.FullPath);
}
private async Task<bool> HasUpdate()
{
if (Package == null)
return false;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
return false;
var canCheckUpdate = Package.LastUpdateCheck == null ||
Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15);
if (!canCheckUpdate)
{
return Package.UpdateAvailable;
}
try
{
var hasUpdate = await basePackage.CheckForUpdates(Package);
Package.UpdateAvailable = hasUpdate;
Package.LastUpdateCheck = DateTimeOffset.Now;
settingsManager.SetLastUpdateCheck(Package);
return hasUpdate;
}
catch (Exception e)
{
logger.Error(e, $"Error checking {Package.PackageName} for updates");
return false;
}
}
/// <summary>
/// Deletes a directory and all of its contents recursively.
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff.
/// </summary>
/// <param name="targetDirectory"></param>
private Task DeleteDirectoryAsync(string targetDirectory)
{
var policy = Policy.Handle<IOException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
onRetry: (exception, calculatedWaitDuration) =>
{
logger.Warn(
exception,
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}",
targetDirectory, calculatedWaitDuration);
});
return policy.ExecuteAsync(async () =>
{
await Task.Run(() =>
{
DeleteDirectory(targetDirectory);
});
});
}
private void DeleteDirectory(string targetDirectory)
{
// Skip if directory does not exist
if (!Directory.Exists(targetDirectory))
{
return;
}
// For junction points, delete with recursive false
if (new DirectoryInfo(targetDirectory).LinkTarget != null)
{
logger.Info("Removing junction point {TargetDirectory}", targetDirectory);
try
{
Directory.Delete(targetDirectory, false);
return;
}
catch (IOException ex)
{
throw new IOException($"Failed to delete junction point {targetDirectory}", ex);
}
}
// Recursively delete all subdirectories
var subdirectoryEntries = Directory.GetDirectories(targetDirectory);
foreach (var subdirectoryPath in subdirectoryEntries)
{
DeleteDirectory(subdirectoryPath);
}
// Delete all files in the directory
var fileEntries = Directory.GetFiles(targetDirectory);
foreach (var filePath in fileEntries)
{
try
{
File.SetAttributes(filePath, FileAttributes.Normal);
File.Delete(filePath);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete file {filePath}", ex);
}
}
// Delete the target directory itself
try
{
Directory.Delete(targetDirectory, false);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete directory {targetDirectory}", ex);
}
}
}

314
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

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

3
StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs

@ -1,5 +1,6 @@
using System; using System;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
@ -44,6 +45,8 @@ public partial class ProgressItemViewModel : ViewModelBase
return Failed ? "Download Failed" : "Downloading..."; return Failed ? "Download Failed" : "Downloading...";
case ProgressType.Extract: case ProgressType.Extract:
return Failed ? "Extraction Failed" : "Extracting..."; return Failed ? "Extraction Failed" : "Extracting...";
case ProgressType.Update:
return Failed ? "Update Failed" : "Updating...";
} }
if (Failed) if (Failed)

1
StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs

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

1
StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs

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

21
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -19,6 +19,7 @@ using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
@ -64,6 +65,21 @@ public partial class SettingsViewModel : PageViewModelBase
"System", "System",
}; };
public IReadOnlyList<float> AnimationScaleOptions { get; } = new[]
{
0f,
0.25f,
0.5f,
0.75f,
1f,
1.25f,
1.5f,
1.75f,
2f,
};
[ObservableProperty] private float selectedAnimationScale;
// Shared folder options // Shared folder options
[ObservableProperty] private bool removeSymlinksOnShutdown; [ObservableProperty] private bool removeSymlinksOnShutdown;
@ -99,6 +115,7 @@ public partial class SettingsViewModel : PageViewModelBase
SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1]; SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1];
RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown; RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown;
SelectedAnimationScale = settingsManager.Settings.AnimationScale;
settingsManager.RelayPropertyFor(this, settingsManager.RelayPropertyFor(this,
vm => vm.SelectedTheme, vm => vm.SelectedTheme,
@ -107,6 +124,10 @@ public partial class SettingsViewModel : PageViewModelBase
settingsManager.RelayPropertyFor(this, settingsManager.RelayPropertyFor(this,
vm => vm.IsDiscordRichPresenceEnabled, vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled); settings => settings.IsDiscordRichPresenceEnabled);
settingsManager.RelayPropertyFor(this,
vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale);
} }
partial void OnSelectedThemeChanged(string? value) partial void OnSelectedThemeChanged(string? value)

39
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

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

7
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

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

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

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

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

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

2
StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml

@ -148,7 +148,7 @@
<StackPanel Margin="0,16,0,0" Orientation="Horizontal"> <StackPanel Margin="0,16,0,0" Orientation="Horizontal">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<Label Content="{Binding ReleaseLabelText, FallbackValue=Version}" /> <Label Content="{Binding ReleaseLabelText}" />
<ComboBox <ComboBox
ItemsSource="{Binding AvailableVersions}" ItemsSource="{Binding AvailableVersions}"
MinWidth="200" MinWidth="200"

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

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

15
StabilityMatrix.Avalonia/Views/MainWindow.axaml

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

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

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

279
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

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

11
StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml

@ -39,6 +39,7 @@
<TextBlock Text="{Binding Name, Mode=OneWay}" <TextBlock Text="{Binding Name, Mode=OneWay}"
Margin="0, 4" /> Margin="0, 4" />
<ProgressBar Value="{Binding Progress.Percentage, Mode=OneWay}" <ProgressBar Value="{Binding Progress.Percentage, Mode=OneWay}"
IsIndeterminate="{Binding Progress.IsIndeterminate}"
Margin="0, 4"> Margin="0, 4">
<ProgressBar.Transitions> <ProgressBar.Transitions>
<Transitions> <Transitions>
@ -50,10 +51,18 @@
</Transitions> </Transitions>
</ProgressBar.Transitions> </ProgressBar.Transitions>
</ProgressBar> </ProgressBar>
<TextBlock Margin="0, 4">
<!-- non-indeterminate progress -->
<TextBlock Margin="0, 4"
IsVisible="{Binding !Progress.IsIndeterminate}">
<Run Text="{Binding ProgressText, Mode=OneWay}" /> <Run Text="{Binding ProgressText, Mode=OneWay}" />
<Run Text="{Binding Progress.Percentage, Mode=OneWay}" /><Run Text="%" /> <Run Text="{Binding Progress.Percentage, Mode=OneWay}" /><Run Text="%" />
</TextBlock> </TextBlock>
<!-- indeterminate progress -->
<TextBlock Margin="0, 4"
IsVisible="{Binding Progress.IsIndeterminate}"
Text="{Binding ProgressText, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Border> </Border>
</DataTemplate> </DataTemplate>

128
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

@ -14,46 +14,44 @@
<Grid> <Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid> <Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto"
<StackPanel Spacing="2" Margin="12,16"> Margin="8, 16">
<!-- Theme --> <!-- Theme -->
<Grid RowDefinitions="auto,*"> <Grid Grid.Row="0" RowDefinitions="auto,*">
<TextBlock <TextBlock
FontWeight="Medium" FontWeight="Medium"
Text="Appearance" Text="Appearance"
Margin="0,0,0,8" /> Margin="0,0,0,8" />
<StackPanel Grid.Row="1">
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1"
Header="Theme" Header="Theme"
IconSource="WeatherMoon" IconSource="WeatherMoon"
Margin="8,0"> Margin="8,0,8,4">
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<ComboBox <ComboBox
ItemsSource="{Binding AvailableThemes}" ItemsSource="{Binding AvailableThemes}"
SelectedItem="{Binding SelectedTheme}" SelectedItem="{Binding SelectedTheme}"
Margin="8"
MinWidth="100"/> MinWidth="100"/>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
</StackPanel>
</Grid> </Grid>
<!-- TODO: Text2Image host port settings --> <!-- TODO: Text2Image host port settings -->
<!-- Checkpoints Manager Options --> <!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*,Auto"> <Grid Grid.Row="1" Margin="0,8,0,0" RowDefinitions="auto,*,Auto">
<TextBlock <TextBlock
FontWeight="Medium" FontWeight="Medium"
Text="Checkpoint Manager" Text="Checkpoint Manager"
Margin="0,16,0,8" /> Margin="0,0,0,8" />
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
IconSource="Folder" IconSource="Folder"
Header="Remove shared checkpoints directory symbolic links on shutdown" Header="Remove shared checkpoints directory symbolic links on shutdown"
Description="Select this option if you're having problems moving Stability Matrix to another drive" Description="Select this option if you're having problems moving Stability Matrix to another drive"
Margin="8,8"> Margin="8,0">
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<CheckBox Margin="4" <CheckBox Margin="8"
IsChecked="{Binding RemoveSymlinksOnShutdown}"/> IsChecked="{Binding RemoveSymlinksOnShutdown}"/>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
@ -62,35 +60,34 @@
IconSource="Refresh" IconSource="Refresh"
Header="Reset Checkpoints Cache" Header="Reset Checkpoints Cache"
Description="Rebuilds the installed checkpoints cache. Use if checkpoints are incorrectly labeled in the Model Browser." Description="Rebuilds the installed checkpoints cache. Use if checkpoints are incorrectly labeled in the Model Browser."
Margin="8,0"> Margin="8, 4">
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<Button Margin="8" <Button Command="{Binding ResetCheckpointCache}"
Command="{Binding ResetCheckpointCache}"
Content="Reset Checkpoints Cache"/> Content="Reset Checkpoints Cache"/>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
</Grid> </Grid>
<!-- Environment Options --> <!-- Environment Options -->
<Grid RowDefinitions="auto,*"> <Grid Grid.Row="2" Margin="0,8,0,0" RowDefinitions="Auto, Auto, Auto">
<TextBlock <TextBlock
FontWeight="Medium" FontWeight="Medium"
Text="Package Environment" Text="Package Environment"
Margin="0,16,0,0" /> Margin="0,0,0,8" />
<StackPanel Grid.Row="1" Margin="0,8" Spacing="4"> <ui:SettingsExpander Grid.Row="1"
<ui:SettingsExpander
Header="Environment Variables" Header="Environment Variables"
IconSource="OtherUser" IconSource="OtherUser"
Margin="8,0"> Margin="8,0">
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<Button Content="Edit" Command="{Binding OpenEnvVarsDialogCommand}"/> <Button Content="Edit"
Command="{Binding OpenEnvVarsDialogCommand}"/>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
<ui:SettingsExpander <ui:SettingsExpander Grid.Row="2"
Header="Embedded Python" Header="Embedded Python"
Margin="8,0"> Margin="8,4">
<ui:SettingsExpander.IconSource> <ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-python"/> <controls:FASymbolIconSource Symbol="fa-brands fa-python"/>
</ui:SettingsExpander.IconSource> </ui:SettingsExpander.IconSource>
@ -105,44 +102,40 @@
</StackPanel> </StackPanel>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
</StackPanel>
</Grid> </Grid>
<!-- Integrations --> <!-- Integrations -->
<Grid RowDefinitions="auto,*"> <Grid Grid.Row="3" Margin="0,8,0,0" RowDefinitions="auto,*">
<TextBlock <TextBlock
FontWeight="Medium" FontWeight="Medium"
Text="Integrations" Text="Integrations"
Margin="0,16,0,8" /> Margin="0,0,0,8" />
<StackPanel Grid.Row="1"> <ui:SettingsExpander Grid.Row="1"
<ui:SettingsExpander Header="Discord Rich Presence"
Header="Discord Rich Presence" Margin="8,0,8,4">
Margin="8,0"> <ui:SettingsExpander.IconSource>
<ui:SettingsExpander.IconSource> <controls:FASymbolIconSource Symbol="fa-brands fa-discord"/>
<controls:FASymbolIconSource Symbol="fa-brands fa-discord"/> </ui:SettingsExpander.IconSource>
</ui:SettingsExpander.IconSource> <ui:SettingsExpander.Footer>
<ui:SettingsExpander.Footer> <ToggleSwitch
<ToggleSwitch IsChecked="{Binding IsDiscordRichPresenceEnabled}" />
IsChecked="{Binding IsDiscordRichPresenceEnabled}" </ui:SettingsExpander.Footer>
Margin="4" /> </ui:SettingsExpander>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</Grid> </Grid>
<!-- System Options --> <!-- System Options -->
<Grid RowDefinitions="auto,*"> <Grid Grid.Row="4" Margin="0,8,0,0" RowDefinitions="auto,*">
<TextBlock <TextBlock
FontWeight="Medium" FontWeight="Medium"
Text="System" Text="System"
Margin="0,16,0,0" /> Margin="0,0,0,8" />
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
ToolTip.Tip="{OnPlatform Default='Only available on Windows', Windows={x:Null}}" ToolTip.Tip="{OnPlatform Default='Only available on Windows', Windows={x:Null}}"
Header="Add Stability Matrix to the Start Menu" Header="Add Stability Matrix to the Start Menu"
Description="Uses the current app location, you can run this again if you move the app" Description="Uses the current app location, you can run this again if you move the app"
IconSource="StarAdd" IconSource="StarAdd"
Margin="8"> Margin="8,0,8,4">
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<controls:ProgressRing <controls:ProgressRing
@ -176,57 +169,84 @@
</Grid> </Grid>
<!-- Debug Options --> <!-- Debug Options -->
<Grid RowDefinitions="auto,*" <Grid Grid.Row="5" RowDefinitions="auto,*"
Margin="0,4,0,0" Margin="0,8,0,0"
IsVisible="{Binding SharedState.IsDebugMode}" > IsVisible="{Binding SharedState.IsDebugMode}" >
<TextBlock <TextBlock
FontWeight="Medium" FontWeight="Medium"
Text="Debug Options" Text="Debug Options"
Margin="0,16,0,0" /> Margin="0,0,0,8" />
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
IconSource="Code" IconSource="Code"
Command="{Binding LoadDebugInfo}" Command="{Binding LoadDebugInfo}"
Header="Debug Options" Header="Debug Options"
Margin="8"> Margin="8, 0,8,4">
<ui:SettingsExpanderItem Description="Paths" IconSource="Folder"> <ui:SettingsExpanderItem Description="Paths" IconSource="Folder"
Margin="4">
<SelectableTextBlock Text="{Binding DebugPaths}" <SelectableTextBlock Text="{Binding DebugPaths}"
Foreground="{DynamicResource TextControlPlaceholderForeground}" Foreground="{DynamicResource TextControlPlaceholderForeground}"
TextWrapping="WrapWithOverflow" /> TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Description="Compat Info" IconSource="StarFilled"> <ui:SettingsExpanderItem Description="Compat Info" IconSource="StarFilled"
Margin="4,0,4,4">
<SelectableTextBlock Text="{Binding DebugCompatInfo}" <SelectableTextBlock Text="{Binding DebugCompatInfo}"
Foreground="{DynamicResource TextControlPlaceholderForeground}" Foreground="{DynamicResource TextControlPlaceholderForeground}"
TextWrapping="WrapWithOverflow" /> TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Description="GPU Info" IconSource="FullScreenMaximize"> <ui:SettingsExpanderItem Description="GPU Info" IconSource="FullScreenMaximize"
Margin="4,0,4,4">
<SelectableTextBlock Text="{Binding DebugGpuInfo}" <SelectableTextBlock Text="{Binding DebugGpuInfo}"
Foreground="{DynamicResource TextControlPlaceholderForeground}" Foreground="{DynamicResource TextControlPlaceholderForeground}"
TextWrapping="WrapWithOverflow" /> TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Notification" IconSource="CommentAdd"> <ui:SettingsExpanderItem Content="Animation Scale" IconSource="Clock"
Description="Lower values = faster animations. 0x means animations are instant."
Margin="4,0,4,4">
<ui:SettingsExpanderItem.Footer>
<ComboBox Margin="0, 8"
ItemsSource="{Binding AnimationScaleOptions}"
SelectedItem="{Binding SelectedAnimationScale}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding }"/><Run Text="x"/>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Notification" IconSource="CommentAdd"
Margin="4,0,4,4">
<ui:SettingsExpanderItem.Footer> <ui:SettingsExpanderItem.Footer>
<Button <Button
Margin="0, 8"
Command="{Binding DebugNotificationCommand}" Command="{Binding DebugNotificationCommand}"
Content="New Notification"/> Content="New Notification"/>
</ui:SettingsExpanderItem.Footer> </ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Content Dialog" IconSource="NewWindow"> <ui:SettingsExpanderItem Content="Content Dialog" IconSource="NewWindow"
Margin="4,0,4,4">
<ui:SettingsExpanderItem.Footer> <ui:SettingsExpanderItem.Footer>
<Button <Button
Margin="0, 8"
Command="{Binding DebugContentDialogCommand}" Command="{Binding DebugContentDialogCommand}"
Content="Show Dialog"/> Content="Show Dialog"/>
</ui:SettingsExpanderItem.Footer> </ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Exceptions" IconSource="Flag"> <ui:SettingsExpanderItem Content="Exceptions" IconSource="Flag"
Margin="4,0,4,4">
<ui:SettingsExpanderItem.Footer> <ui:SettingsExpanderItem.Footer>
<Button <Button
Margin="0, 8"
Command="{Binding DebugThrowExceptionCommand}" Command="{Binding DebugThrowExceptionCommand}"
Content="Unhandled Exception"/> Content="Unhandled Exception"/>
</ui:SettingsExpanderItem.Footer> </ui:SettingsExpanderItem.Footer>
@ -241,7 +261,7 @@
<!-- TODO: Directories card --> <!-- TODO: Directories card -->
<Grid RowDefinitions="auto,*" Margin="0,4,0,0"> <Grid Grid.Row="6" RowDefinitions="auto,*" Margin="0,4,0,0">
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
HorizontalAlignment="Left" HorizontalAlignment="Left"
@ -288,9 +308,7 @@
</Grid> </Grid>
<!-- Extra space at the bottom --> <!-- Extra space at the bottom -->
<Panel Margin="0,0,0,16" /> <Panel Grid.Row="7" Margin="0,0,0,16" />
</StackPanel>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>

15
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -115,8 +115,19 @@ public class LiteDbContext : ILiteDbContext
public void Dispose() public void Dispose()
{ {
database?.UnderlyingDatabase.Dispose(); if (database is not null)
database?.Dispose(); {
try
{
database.Dispose();
}
catch (ObjectDisposedException)
{
}
database = null;
}
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }

4
StabilityMatrix.Core/Extensions/StringExtensions.cs

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

1
StabilityMatrix.Core/Helper/Compat.cs

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

4
StabilityMatrix.Core/Helper/EventManager.cs

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

1
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

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

8
StabilityMatrix.Core/Helper/PrerequisiteHelper.cs

@ -56,14 +56,14 @@ public class PrerequisiteHelper : IPrerequisiteHelper
public async Task RunGit(string? workingDirectory = null, params string[] args) public async Task RunGit(string? workingDirectory = null, params string[] args)
{ {
var process = ProcessRunner.StartAnsiProcess(GitExePath, args, workingDirectory: workingDirectory); var process = ProcessRunner.StartAnsiProcess(GitExePath, args, workingDirectory: workingDirectory);
await ProcessRunner.WaitForExitConditionAsync(process); await ProcessRunner.WaitForExitConditionAsync(process).ConfigureAwait(false);
} }
public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args) public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
{ {
var process = await ProcessRunner.GetProcessOutputAsync(GitExePath, string.Join(" ", args), var output = await ProcessRunner.GetProcessOutputAsync(GitExePath, string.Join(" ", args),
workingDirectory: workingDirectory); workingDirectory: workingDirectory).ConfigureAwait(false);
return process; return output;
} }
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null) public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)

7
StabilityMatrix.Core/Helper/SharedFolders.cs

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

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

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

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

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

2
StabilityMatrix.Core/Models/InstalledPackage.cs

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

6
StabilityMatrix.Core/Models/PackageVersion.cs

@ -1,7 +1,7 @@
namespace StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Models;
public class PackageVersion public record PackageVersion
{ {
public string TagName { get; set; } public required string TagName { get; set; }
public string ReleaseNotesMarkdown { get; set; } public string? ReleaseNotesMarkdown { get; set; }
} }

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

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

20
StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs

@ -240,6 +240,7 @@ public abstract class BaseGitPackage : BasePackage
public override async Task<string> Update(InstalledPackage installedPackage, public override async Task<string> Update(InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null, bool includePrerelease = false) IProgress<ProgressReport>? progress = null, bool includePrerelease = false)
{ {
// Release mode
if (string.IsNullOrWhiteSpace(installedPackage.InstalledBranch)) if (string.IsNullOrWhiteSpace(installedPackage.InstalledBranch))
{ {
var releases = await GetAllReleases().ConfigureAwait(false); var releases = await GetAllReleases().ConfigureAwait(false);
@ -248,15 +249,20 @@ public abstract class BaseGitPackage : BasePackage
await InstallPackage(progress).ConfigureAwait(false); await InstallPackage(progress).ConfigureAwait(false);
return latestRelease.TagName; return latestRelease.TagName;
} }
else
// Commit mode
var allCommits = await GetAllCommits(
installedPackage.InstalledBranch).ConfigureAwait(false);
var latestCommit = allCommits?.First();
if (latestCommit is null || string.IsNullOrEmpty(latestCommit.Sha))
{ {
var allCommits = await GetAllCommits( throw new Exception("No commits found for branch");
installedPackage.InstalledBranch).ConfigureAwait(false);
var latestCommit = allCommits.First();
await DownloadPackage(latestCommit.Sha, true, progress);
await InstallPackage(progress).ConfigureAwait(false);
return latestCommit.Sha;
} }
await DownloadPackage(latestCommit.Sha, true, progress).ConfigureAwait(false);
await InstallPackage(progress).ConfigureAwait(false);
return latestCommit.Sha;
} }
// Send input to the running process. // Send input to the running process.

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

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

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

@ -92,7 +92,7 @@ public class ComfyUI : BaseGitPackage
public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true)
{ {
var allBranches = await GetAllBranches(); var allBranches = await GetAllBranches().ConfigureAwait(false);
return allBranches.Select(b => new PackageVersion return allBranches.Select(b => new PackageVersion
{ {
TagName = $"{b.Name}", TagName = $"{b.Name}",
@ -225,6 +225,9 @@ public class ComfyUI : BaseGitPackage
public override Task UpdateModelFolders(DirectoryPath installDirectory) => public override Task UpdateModelFolders(DirectoryPath installDirectory) =>
SetupModelFolders(installDirectory); SetupModelFolders(installDirectory);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) =>
Task.CompletedTask;
public class ComfyModelPathsYaml public class ComfyModelPathsYaml
{ {
public class SmData public class SmData

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

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

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

@ -1,9 +1,10 @@
using System.Data; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NLog; using NLog;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -28,9 +29,6 @@ public class InvokeAI : BaseGitPackage
public override string Blurb => "Professional Creative Tools for Stable Diffusion"; public override string Blurb => "Professional Creative Tools for Stable Diffusion";
public override string LaunchCommand => "invokeai-web"; public override string LaunchCommand => "invokeai-web";
public override string Disclaimer =>
"Note: InvokeAI support is currently experimental, and checkpoints in the shared models folder will not be available with InvokeAI.";
public override IReadOnlyList<string> ExtraLaunchCommands => new[] public override IReadOnlyList<string> ExtraLaunchCommands => new[]
{ {
"invokeai-configure", "invokeai-configure",
@ -58,41 +56,10 @@ public class InvokeAI : BaseGitPackage
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new() public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
{ {
[SharedFolderType.StableDiffusion] = new[] [SharedFolderType.StableDiffusion] = new[] { RelativeRootPath + "/autoimport/main" },
{ [SharedFolderType.Lora] = new[] { RelativeRootPath + "/autoimport/lora" },
RelativeRootPath + "/models/sd-1/main", [SharedFolderType.TextualInversion] = new[] { RelativeRootPath + "/autoimport/embedding" },
RelativeRootPath + "/models/sd-2/main", [SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" },
RelativeRootPath + "/models/sdxl/main",
RelativeRootPath + "/models/sdxl-refiner/main",
},
[SharedFolderType.Lora] = new[]
{
RelativeRootPath + "/models/sd-1/lora",
RelativeRootPath + "/models/sd-2/lora",
RelativeRootPath + "/models/sdxl/lora",
RelativeRootPath + "/models/sdxl-refiner/lora",
},
[SharedFolderType.TextualInversion] = new[]
{
RelativeRootPath + "/models/sd-1/embedding",
RelativeRootPath + "/models/sd-2/embedding",
RelativeRootPath + "/models/sdxl/embedding",
RelativeRootPath + "/models/sdxl-refiner/embedding",
},
[SharedFolderType.VAE] = new[]
{
RelativeRootPath + "/models/sd-1/vae",
RelativeRootPath + "/models/sd-2/vae",
RelativeRootPath + "/models/sdxl/vae",
RelativeRootPath + "/models/sdxl-refiner/vae",
},
[SharedFolderType.ControlNet] = new[]
{
RelativeRootPath + "/models/sd-1/controlnet",
RelativeRootPath + "/models/sd-2/controlnet",
RelativeRootPath + "/models/sdxl/controlnet",
RelativeRootPath + "/models/sdxl-refiner/controlnet",
},
}; };
// https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md // https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md
@ -218,6 +185,11 @@ public class InvokeAI : BaseGitPackage
{ {
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
if (installedPackage.FullPath is null)
{
throw new NullReferenceException("Installed package path is null");
}
await using var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath, "venv")); await using var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath, "venv"));
venvRunner.WorkingDirectory = installedPackage.FullPath; venvRunner.WorkingDirectory = installedPackage.FullPath;
venvRunner.EnvironmentVariables = GetEnvVars(installedPackage.FullPath); venvRunner.EnvironmentVariables = GetEnvVars(installedPackage.FullPath);
@ -293,10 +265,12 @@ public class InvokeAI : BaseGitPackage
{ {
await SetupVenv(installedPackagePath).ConfigureAwait(false); await SetupVenv(installedPackagePath).ConfigureAwait(false);
if (command.Equals("invokeai-configure")) arguments = command switch
{ {
arguments = "--yes --skip-sd-weights"; "invokeai-configure" => "--yes --skip-sd-weights",
} "invokeai-model-install" => "--yes",
_ => arguments
};
VenvRunner.EnvironmentVariables = GetEnvVars(installedPackagePath); VenvRunner.EnvironmentVariables = GetEnvVars(installedPackagePath);
@ -355,11 +329,20 @@ public class InvokeAI : BaseGitPackage
public override Task SetupModelFolders(DirectoryPath installDirectory) public override Task SetupModelFolders(DirectoryPath installDirectory)
{ {
StabilityMatrix.Core.Helper.SharedFolders
.SetupLinks(SharedFolders, SettingsManager.ModelsDirectory, installDirectory);
return Task.CompletedTask; return Task.CompletedTask;
} }
public override Task UpdateModelFolders(DirectoryPath installDirectory) public override async Task UpdateModelFolders(DirectoryPath installDirectory)
{
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; return Task.CompletedTask;
} }

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

@ -294,53 +294,51 @@ public class VladAutomatic : BaseGitPackage
return Task.CompletedTask; return Task.CompletedTask;
} }
public override Task UpdateModelFolders(DirectoryPath installDirectory) public override Task UpdateModelFolders(DirectoryPath installDirectory) =>
{ SetupModelFolders(installDirectory);
return SetupModelFolders(installDirectory);
} public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) =>
Task.CompletedTask;
public override async Task<string> Update(InstalledPackage installedPackage, public override async Task<string> Update(InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null, bool includePrerelease = false) IProgress<ProgressReport>? progress = null, bool includePrerelease = false)
{ {
if (installedPackage.InstalledBranch is null)
{
throw new Exception("Installed branch is null");
}
progress?.Report(new ProgressReport(0.1f, message: "Downloading package update...", progress?.Report(new ProgressReport(0.1f, message: "Downloading package update...",
isIndeterminate: true, type: ProgressType.Download)); isIndeterminate: true, type: ProgressType.Download));
var version = await GithubApi.GetAllCommits(Author, Name, installedPackage.InstalledBranch); var version = await GithubApi.GetAllCommits(Author, Name, installedPackage.InstalledBranch).ConfigureAwait(false);
var latest = version?.FirstOrDefault(); var latest = version?.FirstOrDefault();
if (latest == null) if (latest?.Sha is null)
{ {
Logger.Warn("No latest version found for vlad"); throw new Exception("Could not get latest version");
return string.Empty;
} }
try try
{ {
var output = var output =
await PrerequisiteHelper.GetGitOutput(workingDirectory: installedPackage.FullPath, await PrerequisiteHelper
"rev-parse", "HEAD"); .GetGitOutput(installedPackage.FullPath, "rev-parse", "HEAD")
.ConfigureAwait(false);
if (output?.Replace("\n", "") == latest.Sha) if (output.Replace(Environment.NewLine, "") == latest.Sha)
{ {
return latest.Sha; return latest.Sha;
} }
} }
catch (Exception)
{
// ignored
}
try
{
await PrerequisiteHelper.RunGit(workingDirectory: installedPackage.FullPath, "pull",
"origin", installedPackage.InstalledBranch);
}
catch (Exception e) catch (Exception e)
{ {
Logger.Log(LogLevel.Error, e); Logger.Warn(e, "Could not get current git hash, continuing with update");
return string.Empty;
} }
await PrerequisiteHelper.RunGit(installedPackage.FullPath, "pull",
"origin", installedPackage.InstalledBranch).ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, message: "Update Complete", isIndeterminate: false, progress?.Report(new ProgressReport(1f, message: "Update Complete", isIndeterminate: false,
type: ProgressType.Generic)); type: ProgressType.Generic));

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

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

1
StabilityMatrix.Core/Models/Progress/ProgressType.cs

@ -5,4 +5,5 @@ public enum ProgressType
Generic, Generic,
Download, Download,
Extract, Extract,
Update,
} }

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

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

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

@ -51,6 +51,8 @@ public class Settings
public HashSet<string>? InstalledModelHashes { get; set; } = new(); public HashSet<string>? InstalledModelHashes { get; set; } = new();
public float AnimationScale { get; set; } = 1.0f;
public void RemoveInstalledPackageAndUpdateActive(InstalledPackage package) public void RemoveInstalledPackageAndUpdateActive(InstalledPackage package)
{ {
RemoveInstalledPackageAndUpdateActive(package.Id); RemoveInstalledPackageAndUpdateActive(package.Id);

3
StabilityMatrix.Core/Processes/ApcMessage.cs

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

1
StabilityMatrix.Core/Processes/ApcParser.cs

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

7
StabilityMatrix.Core/Python/PyRunner.cs

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

7
StabilityMatrix.Core/Python/PyVenvRunner.cs

@ -331,11 +331,12 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
// Disable pip caching - uses significant memory for large packages like torch // Disable pip caching - uses significant memory for large packages like torch
env["PIP_NO_CACHE_DIR"] = "true"; env["PIP_NO_CACHE_DIR"] = "true";
// On windows, add portable git // On windows, add portable git to PATH and binary as GIT
if (Compat.IsWindows) if (Compat.IsWindows)
{ {
var portableGit = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin"); var portableGitBin = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin");
env["PATH"] = Compat.GetEnvPathWithExtensions(portableGit); env["PATH"] = Compat.GetEnvPathWithExtensions(portableGitBin);
env["GIT"] = portableGitBin.JoinFile("git.exe");
} }
if (unbuffered) if (unbuffered)

5
StabilityMatrix.Core/Services/SettingsManager.cs

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

14
StabilityMatrix.Tests/Helper/EventManagerTests.cs

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

Loading…
Cancel
Save