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

2
StabilityMatrix.Avalonia/Controls/AppWindowBase.cs

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

2
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

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

2
StabilityMatrix.Avalonia/Controls/UserControlBase.cs

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

135
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,12 +4,16 @@ using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Helper;
@ -21,6 +25,8 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile;
using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
namespace StabilityMatrix.Avalonia.DesignData;
@ -28,15 +34,16 @@ namespace StabilityMatrix.Avalonia.DesignData;
public static class DesignData
{
[NotNull] public static IServiceProvider? Services { get; set; }
private static bool isInitialized;
// This needs to be static method instead of static constructor
// or else Avalonia analyzers won't work.
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 activePackageId = Guid.NewGuid();
@ -50,6 +57,7 @@ public static class DesignData
{
Id = activePackageId,
DisplayName = "My Installed Package",
DisplayVersion = "v1.0.0",
PackageName = "stable-diffusion-webui",
PackageVersion = "v1.0.0",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
@ -59,8 +67,8 @@ public static class DesignData
{
Id = Guid.NewGuid(),
DisplayName = "Dank Diffusion",
PackageName = "dank-diffusion",
PackageVersion = "v2.0.0",
PackageName = "ComfyUI",
DisplayVersion = "main@ab73d4a",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
}
@ -68,14 +76,15 @@ public static class DesignData
ActiveInstalledPackageId = activePackageId
}
});
// General services
services.AddLogging()
.AddSingleton<INavigationService, NavigationService>()
.AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<IUpdateHelper, UpdateHelper>()
.AddSingleton<ModelFinder>()
.AddSingleton<SharedState>();
// Mock services
services
.AddSingleton<INotificationService, MockNotificationService>()
@ -83,7 +92,7 @@ public static class DesignData
.AddSingleton<IDownloadService, MockDownloadService>()
.AddSingleton<IHttpClientFactory, MockHttpClientFactory>()
.AddSingleton<IDiscordRichPresenceService, MockDiscordRichPresenceService>();
// Placeholder services that nobody should need during design time
services
.AddSingleton<IPyRunner>(_ => null!)
@ -91,17 +100,17 @@ public static class DesignData
.AddSingleton<ICivitApi>(_ => null!)
.AddSingleton<IGithubApiCache>(_ => null!)
.AddSingleton<IPrerequisiteHelper>(_ => null!);
// Using some default service implementations from App
App.ConfigurePackages(services);
App.ConfigurePageViewModels(services);
App.ConfigureDialogViewModels(services);
App.ConfigureViews(services);
// Override Launch page with mock
services.Remove(ServiceDescriptor.Singleton<LaunchPageViewModel, LaunchPageViewModel>());
services.AddSingleton<LaunchPageViewModel, MockLaunchPageViewModel>();
Services = services.BuildServiceProvider();
var dialogFactory = Services.GetRequiredService<ServiceManager<ViewModelBase>>();
@ -110,7 +119,7 @@ public static class DesignData
var modelFinder = Services.GetRequiredService<ModelFinder>();
var packageFactory = Services.GetRequiredService<IPackageFactory>();
var notificationService = Services.GetRequiredService<INotificationService>();
LaunchOptionsViewModel = Services.GetRequiredService<LaunchOptionsViewModel>();
LaunchOptionsViewModel.Cards = new[]
{
@ -120,13 +129,13 @@ public static class DesignData
Type = LaunchOptionType.String,
Description = "The host name for the Web UI",
DefaultValue = "localhost",
Options = { "--host" }
Options = {"--host"}
}),
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition
{
Name = "API",
Type = LaunchOptionType.Bool,
Options = { "--api" }
Options = {"--api"}
})
};
LaunchOptionsViewModel.UpdateFilterCards();
@ -136,7 +145,7 @@ public static class DesignData
packageFactory.GetAllAvailablePackages().ToImmutableArray();
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
// Checkpoints page
CheckpointsPageViewModel.CheckpointFolders = new ObservableCollection<CheckpointFolder>
{
@ -201,52 +210,76 @@ public static class DesignData
folder.DisplayedCheckpointFiles = folder.CheckpointFiles;
}
CheckpointBrowserViewModel.ModelCards = new
CheckpointBrowserViewModel.ModelCards = new
ObservableCollection<CheckpointBrowserCardViewModel>
{
new(new CivitModel
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = new CivitModel
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
}, downloadService, settingsManager,
dialogFactory, notificationService)
};
})
};
ProgressManagerViewModel.ProgressItems = new ObservableCollection<ProgressItemViewModel>
{
new(new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))),
new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu", new ProgressReport(0.25f, "Extracting...")))
new(new ProgressItem(Guid.NewGuid(), "Test File.exe",
new ProgressReport(0.5f, "Downloading..."))),
new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu",
new ProgressReport(0.25f, "Extracting...")))
};
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.UpdateText =
$"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;
}
[NotNull] public static InstallerViewModel? InstallerViewModel { get; private set; }
[NotNull] public static LaunchOptionsViewModel? LaunchOptionsViewModel { get; private set; }
[NotNull] public static UpdateViewModel? UpdateViewModel { get; private set; }
public static ServiceManager<ViewModelBase> DialogFactory =>
public static ServiceManager<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>();
public static MainWindowViewModel MainWindowViewModel =>
public static MainWindowViewModel MainWindowViewModel =>
Services.GetRequiredService<MainWindowViewModel>();
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
Services.GetRequiredService<FirstLaunchSetupViewModel>();
public static LaunchPageViewModel LaunchPageViewModel =>
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static PackageManagerViewModel PackageManagerViewModel =>
Services.GetRequiredService<PackageManagerViewModel>();
public static CheckpointsPageViewModel CheckpointsPageViewModel =>
public static PackageManagerViewModel PackageManagerViewModel
{
get
{
var settings = Services.GetRequiredService<ISettingsManager>();
var vm = Services.GetRequiredService<PackageManagerViewModel>();
vm.Packages = new ObservableCollection<PackageCardViewModel>(
settings.Settings.InstalledPackages.Select(p =>
DialogFactory.Get<PackageCardViewModel>(viewModel => viewModel.Package = p)));
vm.Packages.First().IsUpdateAvailable = true;
return vm;
}
}
public static CheckpointsPageViewModel CheckpointsPageViewModel =>
Services.GetRequiredService<CheckpointsPageViewModel>();
public static SettingsViewModel SettingsViewModel =>
public static SettingsViewModel SettingsViewModel =>
Services.GetRequiredService<SettingsViewModel>();
public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService<CheckpointBrowserViewModel>();
public static SelectModelVersionViewModel SelectModelVersionViewModel =>
public static SelectModelVersionViewModel SelectModelVersionViewModel =>
DialogFactory.Get<SelectModelVersionViewModel>(vm =>
{
// Sample data
@ -256,6 +289,7 @@ public static class DesignData
{
Name = "BB95 Furry Mix",
Description = "v1.0.0",
BaseModel = "SD 1.5",
Files = new List<CivitFile>
{
new()
@ -289,18 +323,22 @@ public static class DesignData
};
var sampleViewModel =
new ModelVersionViewModel(new HashSet<string> {"ABCD"}, sampleCivitVersions[0]);
// Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel>{sampleViewModel};
vm.Versions = new List<ModelVersionViewModel> {sampleViewModel};
vm.SelectedVersionViewModel = sampleViewModel;
});
public static OneClickInstallViewModel OneClickInstallViewModel =>
public static OneClickInstallViewModel OneClickInstallViewModel =>
Services.GetRequiredService<OneClickInstallViewModel>();
public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel =>
public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel =>
Services.GetRequiredService<SelectDataDirectoryViewModel>();
public static ProgressManagerViewModel ProgressManagerViewModel =>
Services.GetRequiredService<ProgressManagerViewModel>();
public static ExceptionViewModel ExceptionViewModel =>
public static ExceptionViewModel ExceptionViewModel =>
DialogFactory.Get<ExceptionViewModel>(viewModel =>
{
// Use try-catch to generate traceback information
@ -321,13 +359,14 @@ public static class DesignData
}
});
public static EnvVarsViewModel EnvVarsViewModel => DialogFactory.Get<EnvVarsViewModel>(viewModel =>
{
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>
public static EnvVarsViewModel EnvVarsViewModel => DialogFactory.Get<EnvVarsViewModel>(
viewModel =>
{
new("UWU", "TRUE"),
};
});
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>
{
new("UWU", "TRUE"),
};
});
public static RefreshBadgeViewModel RefreshBadgeViewModel => new()
{

1
StabilityMatrix.Avalonia/DesignData/MockLaunchPageViewModel.cs

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

7
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

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

1
StabilityMatrix.Avalonia/Program.cs

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

23
StabilityMatrix.Avalonia/Services/INavigationService.cs

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

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

70
StabilityMatrix.Avalonia/ViewLocator.cs

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

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

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

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

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

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

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

37
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

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

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

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

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

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

15
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

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

7
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

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

17
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

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

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

318
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

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

3
StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs

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

1
StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs

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

1
StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs

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

21
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -19,6 +19,7 @@ using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
@ -63,6 +64,21 @@ public partial class SettingsViewModel : PageViewModelBase
"Dark",
"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
[ObservableProperty] private bool removeSymlinksOnShutdown;
@ -99,6 +115,7 @@ public partial class SettingsViewModel : PageViewModelBase
SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1];
RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown;
SelectedAnimationScale = settingsManager.Settings.AnimationScale;
settingsManager.RelayPropertyFor(this,
vm => vm.SelectedTheme,
@ -107,6 +124,10 @@ public partial class SettingsViewModel : PageViewModelBase
settingsManager.RelayPropertyFor(this,
vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled);
settingsManager.RelayPropertyFor(this,
vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale);
}
partial void OnSelectedThemeChanged(string? value)

39
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

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

7
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

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

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

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

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

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

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

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

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

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

15
StabilityMatrix.Avalonia/Views/MainWindow.axaml

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

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

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

285
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

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

11
StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml

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

128
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

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

15
StabilityMatrix.Core/Database/LiteDbContext.cs

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

4
StabilityMatrix.Core/Extensions/StringExtensions.cs

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

1
StabilityMatrix.Core/Helper/Compat.cs

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

4
StabilityMatrix.Core/Helper/EventManager.cs

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

1
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

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

8
StabilityMatrix.Core/Helper/PrerequisiteHelper.cs

@ -56,14 +56,14 @@ public class PrerequisiteHelper : IPrerequisiteHelper
public async Task RunGit(string? workingDirectory = null, params string[] args)
{
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)
{
var process = await ProcessRunner.GetProcessOutputAsync(GitExePath, string.Join(" ", args),
workingDirectory: workingDirectory);
return process;
var output = await ProcessRunner.GetProcessOutputAsync(GitExePath, string.Join(" ", args),
workingDirectory: workingDirectory).ConfigureAwait(false);
return output;
}
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;
if (sharedFolders == null)
@ -171,7 +171,6 @@ public class SharedFolders : ISharedFolders
public void RemoveLinksForAllPackages()
{
var libraryDir = settingsManager.LibraryDir;
var packages = settingsManager.Settings.InstalledPackages;
foreach (var package in packages)
{
@ -182,11 +181,11 @@ public class SharedFolders : ISharedFolders
try
{
RemoveLinksForPackage(basePackage, Path.Combine(libraryDir, package.LibraryPath));
basePackage.RemoveModelFolderLinks(package.FullPath).GetAwaiter().GetResult();
}
catch (Exception e)
{
Logger.Warn("Failed to remove junction links for package {Package} " +
Logger.Warn("Failed to remove links for package {Package} " +
"({DisplayName}): {Message}", package.PackageName, package.DisplayName, e.Message);
}
}

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

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

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

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

2
StabilityMatrix.Core/Models/InstalledPackage.cs

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

6
StabilityMatrix.Core/Models/PackageVersion.cs

@ -1,7 +1,7 @@
namespace StabilityMatrix.Core.Models;
public class PackageVersion
public record PackageVersion
{
public string TagName { get; set; }
public string ReleaseNotesMarkdown { get; set; }
public required string TagName { 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.RegularExpressions;
using NLog;
@ -207,4 +206,10 @@ public class A3WebUI : BaseGitPackage
await StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(this,
SettingsManager.ModelsDirectory, installDirectory).ConfigureAwait(false);
}
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
{
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(this, installDirectory);
return Task.CompletedTask;
}
}

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

@ -240,6 +240,7 @@ public abstract class BaseGitPackage : BasePackage
public override async Task<string> Update(InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null, bool includePrerelease = false)
{
// Release mode
if (string.IsNullOrWhiteSpace(installedPackage.InstalledBranch))
{
var releases = await GetAllReleases().ConfigureAwait(false);
@ -248,15 +249,20 @@ public abstract class BaseGitPackage : BasePackage
await InstallPackage(progress).ConfigureAwait(false);
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(
installedPackage.InstalledBranch).ConfigureAwait(false);
var latestCommit = allCommits.First();
await DownloadPackage(latestCommit.Sha, true, progress);
await InstallPackage(progress).ConfigureAwait(false);
return latestCommit.Sha;
throw new Exception("No commits found for branch");
}
await DownloadPackage(latestCommit.Sha, true, progress).ConfigureAwait(false);
await InstallPackage(progress).ConfigureAwait(false);
return latestCommit.Sha;
}
// 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 SetupModelFolders(DirectoryPath installDirectory);
public abstract Task UpdateModelFolders(DirectoryPath installDirectory);
public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory);
/// <summary>
/// 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)
{
var allBranches = await GetAllBranches();
var allBranches = await GetAllBranches().ConfigureAwait(false);
return allBranches.Select(b => new PackageVersion
{
TagName = $"{b.Name}",
@ -225,6 +225,9 @@ public class ComfyUI : BaseGitPackage
public override Task UpdateModelFolders(DirectoryPath installDirectory) =>
SetupModelFolders(installDirectory);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) =>
Task.CompletedTask;
public class ComfyModelPathsYaml
{
public class SmData

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

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

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

@ -1,9 +1,10 @@
using System.Data;
using System.Text.Json;
using System.Text.RegularExpressions;
using NLog;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
@ -28,9 +29,6 @@ public class InvokeAI : BaseGitPackage
public override string Blurb => "Professional Creative Tools for Stable Diffusion";
public override string LaunchCommand => "invokeai-web";
public override string Disclaimer =>
"Note: InvokeAI support is currently experimental, and checkpoints in the shared models folder will not be available with InvokeAI.";
public override IReadOnlyList<string> ExtraLaunchCommands => new[]
{
"invokeai-configure",
@ -55,44 +53,13 @@ public class InvokeAI : BaseGitPackage
base(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
}
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
{
[SharedFolderType.StableDiffusion] = new[]
{
RelativeRootPath + "/models/sd-1/main",
RelativeRootPath + "/models/sd-2/main",
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",
},
[SharedFolderType.StableDiffusion] = new[] { RelativeRootPath + "/autoimport/main" },
[SharedFolderType.Lora] = new[] { RelativeRootPath + "/autoimport/lora" },
[SharedFolderType.TextualInversion] = new[] { RelativeRootPath + "/autoimport/embedding" },
[SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" },
};
// 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));
if (installedPackage.FullPath is null)
{
throw new NullReferenceException("Installed package path is null");
}
await using var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath, "venv"));
venvRunner.WorkingDirectory = installedPackage.FullPath;
venvRunner.EnvironmentVariables = GetEnvVars(installedPackage.FullPath);
@ -293,10 +265,12 @@ public class InvokeAI : BaseGitPackage
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
if (command.Equals("invokeai-configure"))
arguments = command switch
{
arguments = "--yes --skip-sd-weights";
}
"invokeai-configure" => "--yes --skip-sd-weights",
"invokeai-model-install" => "--yes",
_ => arguments
};
VenvRunner.EnvironmentVariables = GetEnvVars(installedPackagePath);
@ -355,11 +329,20 @@ public class InvokeAI : BaseGitPackage
public override Task SetupModelFolders(DirectoryPath installDirectory)
{
StabilityMatrix.Core.Helper.SharedFolders
.SetupLinks(SharedFolders, SettingsManager.ModelsDirectory, installDirectory);
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;
}

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

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

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

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

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

@ -51,6 +51,8 @@ public class Settings
public HashSet<string>? InstalledModelHashes { get; set; } = new();
public float AnimationScale { get; set; } = 1.0f;
public void RemoveInstalledPackageAndUpdateActive(InstalledPackage package)
{
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.Serialization;
using NLog;

1
StabilityMatrix.Core/Processes/ApcParser.cs

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

7
StabilityMatrix.Core/Python/PyRunner.cs

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

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
env["PIP_NO_CACHE_DIR"] = "true";
// On windows, add portable git
// On windows, add portable git to PATH and binary as GIT
if (Compat.IsWindows)
{
var portableGit = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin");
env["PATH"] = Compat.GetEnvPathWithExtensions(portableGit);
var portableGitBin = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin");
env["PATH"] = Compat.GetEnvPathWithExtensions(portableGitBin);
env["GIT"] = portableGitBin.JoinFile("git.exe");
}
if (unbuffered)

5
StabilityMatrix.Core/Services/SettingsManager.cs

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

14
StabilityMatrix.Tests/Helper/EventManagerTests.cs

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

Loading…
Cancel
Save