Browse Source

Merge pull request #230 from ionite34/install-refactor

install/update/versioning refactor & new install dialog layout
pull/117/head
JT 1 year ago committed by GitHub
parent
commit
f5bb6abfe3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .husky/task-runner.json
  2. 7
      CHANGELOG.md
  3. 6
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs
  4. 16
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs
  5. 8
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs
  6. 10
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs
  7. 28
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs
  8. 5
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs
  9. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs
  10. 13
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs
  11. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs
  12. 16
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs
  13. 20
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs
  14. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs
  15. 3
      StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs
  16. 9
      StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs
  17. 3
      StabilityMatrix.Avalonia/App.axaml
  18. 9
      StabilityMatrix.Avalonia/App.axaml.cs
  19. 127
      StabilityMatrix.Avalonia/Assets.cs
  20. 72
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  21. 147
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  22. 15
      StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
  23. 4
      StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
  24. 60
      StabilityMatrix.Avalonia/Program.cs
  25. 11
      StabilityMatrix.Avalonia/ViewModels/Base/ConsoleProgressViewModel.cs
  26. 26
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
  27. 3
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs
  28. 13
      StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs
  29. 109
      StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
  30. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs
  31. 367
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
  32. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
  33. 97
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  34. 121
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
  35. 48
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
  36. 46
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  37. 54
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  38. 172
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  39. 28
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  40. 137
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  41. 49
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  42. 2
      StabilityMatrix.Avalonia/ViewModels/Progress/DownloadProgressItemViewModel.cs
  43. 78
      StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
  44. 6
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressItemViewModel.cs
  45. 56
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs
  46. 218
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  47. 373
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  48. 56
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
  49. 51
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs
  50. 6
      StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs
  51. 7
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  52. 69
      StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml
  53. 1
      StabilityMatrix.Core/Database/ILiteDbContext.cs
  54. 77
      StabilityMatrix.Core/Database/LiteDbContext.cs
  55. 40
      StabilityMatrix.Core/Helper/EventManager.cs
  56. 65
      StabilityMatrix.Core/Helper/SharedFolders.cs
  57. 7
      StabilityMatrix.Core/Models/Database/GitCommit.cs
  58. 3
      StabilityMatrix.Core/Models/Database/LocalModelFile.cs
  59. 8
      StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs
  60. 29
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  61. 72
      StabilityMatrix.Core/Models/InstalledPackage.cs
  62. 23
      StabilityMatrix.Core/Models/InstalledPackageVersion.cs
  63. 33
      StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs
  64. 27
      StabilityMatrix.Core/Models/PackageModification/DownloadPackageVersionStep.cs
  65. 14
      StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs
  66. 33
      StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs
  67. 43
      StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs
  68. 9
      StabilityMatrix.Core/Models/PackageModification/PackageStep.cs
  69. 24
      StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs
  70. 32
      StabilityMatrix.Core/Models/PackageModification/SetupModelFoldersStep.cs
  71. 44
      StabilityMatrix.Core/Models/PackageModification/SetupPrerequisitesStep.cs
  72. 1
      StabilityMatrix.Core/Models/PackageVersion.cs
  73. 128
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  74. 228
      StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
  75. 165
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  76. 197
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  77. 49
      StabilityMatrix.Core/Models/Packages/DankDiffusion.cs
  78. 111
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  79. 215
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  80. 7
      StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs
  81. 75
      StabilityMatrix.Core/Models/Packages/UnknownPackage.cs
  82. 312
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  83. 58
      StabilityMatrix.Core/Models/Packages/VoltaML.cs
  84. 7
      StabilityMatrix.Core/Models/Progress/ProgressItem.cs
  85. 8
      StabilityMatrix.Core/Models/SharedFolderMethod.cs
  86. 11
      StabilityMatrix.Core/Models/TorchVersion.cs
  87. 10
      StabilityMatrix.Core/Services/ISettingsManager.cs
  88. 37
      StabilityMatrix.Core/Services/ModelIndexService.cs
  89. 164
      StabilityMatrix.Core/Services/SettingsManager.cs
  90. 34
      StabilityMatrix.Core/Services/TrackedDownloadService.cs
  91. 1
      StabilityMatrix.Core/StabilityMatrix.Core.csproj

2
.husky/task-runner.json

@ -11,7 +11,7 @@
"name": "Run xamlstyler", "name": "Run xamlstyler",
"group": "pre-commit", "group": "pre-commit",
"command": "dotnet", "command": "dotnet",
"args": [ "xstyler", "${staged}" ], "args": [ "xstyler", "-f", "${staged}" ],
"include": [ "**/*.axaml" ] "include": [ "**/*.axaml" ]
} }
] ]

7
CHANGELOG.md

@ -5,6 +5,13 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.4.0
### Changed
- Revamped package installer
- Added "advanced options" section for commit, shared folder method, and pytorch options
- Can be run in the background
- Shows progress in the Downloads tab
## v2.3.4 ## v2.3.4
### Fixed ### Fixed
- Fixed [#108](https://github.com/LykosAI/StabilityMatrix/issues/108) - (Linux) Fixed permission error on updates [#103](https://github.com/LykosAI/StabilityMatrix/pull/103) - Fixed [#108](https://github.com/LykosAI/StabilityMatrix/issues/108) - (Linux) Fixed permission error on updates [#103](https://github.com/LykosAI/StabilityMatrix/pull/103)

6
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs

@ -8,8 +8,7 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Controls;
public partial class LogViewerControl : UserControl public partial class LogViewerControl : UserControl
{ {
public LogViewerControl() public LogViewerControl() => InitializeComponent();
=> InitializeComponent();
private ILogDataStoreImpl? vm; private ILogDataStoreImpl? vm;
private LogModel? item; private LogModel? item;
@ -46,7 +45,8 @@ public partial class LogViewerControl : UserControl
{ {
base.OnDetachedFromLogicalTree(e); base.OnDetachedFromLogicalTree(e);
if (vm is null) return; if (vm is null)
return;
vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged; vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged;
} }
} }

16
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs

@ -13,13 +13,15 @@ public class ChangeColorTypeConverter : IValueConverter
return new SolidColorBrush((Color)(parameter ?? Colors.Black)); return new SolidColorBrush((Color)(parameter ?? Colors.Black));
var sysDrawColor = (SysDrawColor)value!; var sysDrawColor = (SysDrawColor)value!;
return new SolidColorBrush(Color.FromArgb( return new SolidColorBrush(
sysDrawColor.A, Color.FromArgb(sysDrawColor.A, sysDrawColor.R, sysDrawColor.G, sysDrawColor.B)
sysDrawColor.R, );
sysDrawColor.G,
sysDrawColor.B));
} }
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) public object ConvertBack(
=> throw new NotImplementedException(); object? value,
Type targetType,
object? parameter,
CultureInfo culture
) => throw new NotImplementedException();
} }

8
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs

@ -17,6 +17,10 @@ public class EventIdConverter : IValueConverter
} }
// If not implemented, an error is thrown // If not implemented, an error is thrown
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) public object ConvertBack(
=> new EventId(0, value?.ToString() ?? string.Empty); object? value,
Type targetType,
object? parameter,
CultureInfo culture
) => new EventId(0, value?.ToString() ?? string.Empty);
} }

10
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs

@ -4,8 +4,14 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Extensions;
public static class LoggerExtensions public static class LoggerExtensions
{ {
public static void Emit(this ILogger logger, EventId eventId, public static void Emit(
LogLevel logLevel, string message, Exception? exception = null, params object?[] args) this ILogger logger,
EventId eventId,
LogLevel logLevel,
string message,
Exception? exception = null,
params object?[] args
)
{ {
if (logger is null) if (logger is null)
return; return;

28
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs

@ -9,24 +9,13 @@ public class DataStoreLoggerConfiguration
public EventId EventId { get; set; } public EventId EventId { get; set; }
public Dictionary<LogLevel, LogEntryColor> Colors { get; } = new() public Dictionary<LogLevel, LogEntryColor> Colors { get; } =
new()
{ {
[LogLevel.Trace] = new LogEntryColor [LogLevel.Trace] = new LogEntryColor { Foreground = Color.DarkGray },
{ [LogLevel.Debug] = new LogEntryColor { Foreground = Color.Gray },
Foreground = Color.DarkGray [LogLevel.Information] = new LogEntryColor { Foreground = Color.WhiteSmoke, },
}, [LogLevel.Warning] = new LogEntryColor { Foreground = Color.Orange },
[LogLevel.Debug] = new LogEntryColor
{
Foreground = Color.Gray
},
[LogLevel.Information] = new LogEntryColor
{
Foreground = Color.WhiteSmoke,
},
[LogLevel.Warning] = new LogEntryColor
{
Foreground = Color.Orange
},
[LogLevel.Error] = new LogEntryColor [LogLevel.Error] = new LogEntryColor
{ {
Foreground = Color.White, Foreground = Color.White,
@ -37,10 +26,7 @@ public class DataStoreLoggerConfiguration
Foreground = Color.White, Foreground = Color.White,
Background = Color.Red Background = Color.Red
}, },
[LogLevel.None] = new LogEntryColor [LogLevel.None] = new LogEntryColor { Foreground = Color.Magenta }
{
Foreground = Color.Magenta
}
}; };
#endregion #endregion

5
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs

@ -4,9 +4,7 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
public class LogEntryColor public class LogEntryColor
{ {
public LogEntryColor() public LogEntryColor() { }
{
}
public LogEntryColor(Color foreground, Color background) public LogEntryColor(Color foreground, Color background)
{ {
@ -16,5 +14,4 @@ public class LogEntryColor
public Color Foreground { get; set; } = Color.Black; public Color Foreground { get; set; } = Color.Black;
public Color Background { get; set; } = Color.Transparent; public Color Background { get; set; } = Color.Transparent;
} }

4
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs

@ -27,7 +27,5 @@ public class LogModel
#endregion #endregion
public string LoggerDisplayName => public string LoggerDisplayName =>
LoggerName? LoggerName?.Split('.', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? "";
.Split('.', StringSplitOptions.RemoveEmptyEntries)
.LastOrDefault() ?? "";
} }

13
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs

@ -5,9 +5,14 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
public class ObservableObject : INotifyPropertyChanged public class ObservableObject : INotifyPropertyChanged
{ {
protected bool Set<TValue>(ref TValue field, TValue newValue, [CallerMemberName] string? propertyName = null) protected bool Set<TValue>(
ref TValue field,
TValue newValue,
[CallerMemberName] string? propertyName = null
)
{ {
if (EqualityComparer<TValue>.Default.Equals(field, newValue)) return false; if (EqualityComparer<TValue>.Default.Equals(field, newValue))
return false;
field = newValue; field = newValue;
OnPropertyChanged(propertyName); OnPropertyChanged(propertyName);
@ -16,6 +21,6 @@ public class ObservableObject : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }

4
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs

@ -1,3 +1,5 @@
namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
public class ViewModel : ObservableObject { /* skip */ } public class ViewModel
: ObservableObject { /* skip */
}

16
StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs

@ -54,19 +54,27 @@ public class DataStoreLoggerTarget : TargetWithLayout
} }
// add log entry // add log entry
_dataStore?.AddEntry(new LogModel _dataStore?.AddEntry(
new LogModel
{ {
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
LogLevel = logLevel, LogLevel = logLevel,
// do we override the default EventId if it exists? // do we override the default EventId if it exists?
EventId = eventId.Id == 0 && (_config?.EventId.Id ?? 0) != 0 ? _config!.EventId : eventId, EventId =
eventId.Id == 0 && (_config?.EventId.Id ?? 0) != 0 ? _config!.EventId : eventId,
State = message, State = message,
LoggerName = logEvent.LoggerName, LoggerName = logEvent.LoggerName,
CallerClassName = logEvent.CallerClassName, CallerClassName = logEvent.CallerClassName,
CallerMemberName = logEvent.CallerMemberName, CallerMemberName = logEvent.CallerMemberName,
Exception = logEvent.Exception?.Message ?? (logLevel == MsLogLevel.Error ? message : ""), Exception =
logEvent.Exception?.Message ?? (logLevel == MsLogLevel.Error ? message : ""),
Color = _config!.Colors[logLevel], Color = _config!.Colors[logLevel],
}); }
);
Debug.WriteLine(
$"--- [{logLevel.ToString()[..3]}] {message} - {logEvent.Exception?.Message ?? "no error"}"
);
} }
#endregion #endregion

20
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs

@ -23,7 +23,8 @@ public static class ServicesExtension
public static IServiceCollection AddLogViewer( public static IServiceCollection AddLogViewer(
this IServiceCollection services, this IServiceCollection services,
Action<DataStoreLoggerConfiguration> configure) Action<DataStoreLoggerConfiguration> configure
)
{ {
services.AddSingleton<ILogDataStore>(Core.Logging.LogDataStore.Instance); services.AddSingleton<ILogDataStore>(Core.Logging.LogDataStore.Instance);
services.AddSingleton<LogViewerControlViewModel>(); services.AddSingleton<LogViewerControlViewModel>();
@ -32,13 +33,18 @@ public static class ServicesExtension
return services; return services;
} }
public static ILoggingBuilder AddNLogTargets(this ILoggingBuilder builder, IConfiguration config) public static ILoggingBuilder AddNLogTargets(
this ILoggingBuilder builder,
IConfiguration config
)
{ {
LogManager LogManager
.Setup() .Setup()
// Register custom Target // Register custom Target
.SetupExtensions(extensionBuilder => .SetupExtensions(
extensionBuilder.RegisterTarget<DataStoreLoggerTarget>("DataStoreLogger")); extensionBuilder =>
extensionBuilder.RegisterTarget<DataStoreLoggerTarget>("DataStoreLogger")
);
/*builder /*builder
.ClearProviders() .ClearProviders()
@ -56,7 +62,11 @@ public static class ServicesExtension
return builder; return builder;
} }
public static ILoggingBuilder AddNLogTargets(this ILoggingBuilder builder, IConfiguration config, Action<DataStoreLoggerConfiguration> configure) public static ILoggingBuilder AddNLogTargets(
this ILoggingBuilder builder,
IConfiguration config,
Action<DataStoreLoggerConfiguration> configure
)
{ {
builder.AddNLogTargets(config); builder.AddNLogTargets(config);
builder.Services.Configure(configure); builder.Services.Configure(configure);

4
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs

@ -6,8 +6,8 @@ public class LogDataStore : Core.Logging.LogDataStore
{ {
#region Methods #region Methods
public override async void AddEntry(Core.Logging.LogModel logModel) public override async void AddEntry(Core.Logging.LogModel logModel) =>
=> await Dispatcher.UIThread.InvokeAsync(() => base.AddEntry(logModel)); await Dispatcher.UIThread.InvokeAsync(() => base.AddEntry(logModel));
#endregion #endregion
} }

3
StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs

@ -14,7 +14,6 @@ public class LogWindowViewModel
public static LogWindowViewModel FromServiceProvider(IServiceProvider services) public static LogWindowViewModel FromServiceProvider(IServiceProvider services)
{ {
return new LogWindowViewModel( return new LogWindowViewModel(services.GetRequiredService<LogViewerControlViewModel>());
services.GetRequiredService<LogViewerControlViewModel>());
} }
} }

9
StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs

@ -17,12 +17,17 @@ public partial class LogWindow : Window
return Attach(root, serviceProvider, new KeyGesture(Key.F11)); return Attach(root, serviceProvider, new KeyGesture(Key.F11));
} }
public static IDisposable Attach(TopLevel root, IServiceProvider serviceProvider, KeyGesture gesture) public static IDisposable Attach(
TopLevel root,
IServiceProvider serviceProvider,
KeyGesture gesture
)
{ {
return (root ?? throw new ArgumentNullException(nameof(root))).AddDisposableHandler( return (root ?? throw new ArgumentNullException(nameof(root))).AddDisposableHandler(
KeyDownEvent, KeyDownEvent,
PreviewKeyDown, PreviewKeyDown,
RoutingStrategies.Tunnel); RoutingStrategies.Tunnel
);
void PreviewKeyDown(object? sender, KeyEventArgs e) void PreviewKeyDown(object? sender, KeyEventArgs e)
{ {

3
StabilityMatrix.Avalonia/App.axaml

@ -22,7 +22,8 @@
</Application.Resources> </Application.Resources>
<Application.Styles> <Application.Styles>
<styling:FluentAvaloniaTheme PreferUserAccentColor="True" UseSystemFontOnWindows="True" /> <styling:FluentAvaloniaTheme PreferUserAccentColor="True" UseSystemFontOnWindows="True"
TextVerticalAlignmentOverrideBehavior="Disabled"/>
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml"/> <StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml"/>
<StyleInclude Source="avares://AsyncImageLoader.Avalonia/AdvancedImage.axaml" /> <StyleInclude Source="avares://AsyncImageLoader.Avalonia/AdvancedImage.axaml" />
<StyleInclude Source="Styles/ProgressRing.axaml"/> <StyleInclude Source="Styles/ProgressRing.axaml"/>

9
StabilityMatrix.Avalonia/App.axaml.cs

@ -47,6 +47,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager; using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
@ -57,6 +58,7 @@ using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Models.Configs;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
@ -370,6 +372,7 @@ public sealed class App : Application
services.AddSingleton<IUpdateHelper, UpdateHelper>(); services.AddSingleton<IUpdateHelper, UpdateHelper>();
services.AddSingleton<INavigationService, NavigationService>(); services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IModelIndexService, ModelIndexService>(); services.AddSingleton<IModelIndexService, ModelIndexService>();
services.AddTransient<IPackageModificationRunner, PackageModificationRunner>();
services.AddSingleton<ITrackedDownloadService, TrackedDownloadService>(); services.AddSingleton<ITrackedDownloadService, TrackedDownloadService>();
services.AddSingleton<IDisposable>( services.AddSingleton<IDisposable>(
@ -417,7 +420,8 @@ public sealed class App : Application
// if (string.IsNullOrWhiteSpace(githubApiKey)) // if (string.IsNullOrWhiteSpace(githubApiKey))
// return client; // return client;
// //
// client.Credentials = new Credentials(githubApiKey); // client.Credentials =
// new Credentials("");
return client; return client;
}); });
@ -601,6 +605,9 @@ public sealed class App : Application
builder.ForLogger("System.*").WriteToNil(NLog.LogLevel.Warn); builder.ForLogger("System.*").WriteToNil(NLog.LogLevel.Warn);
builder.ForLogger("Microsoft.*").WriteToNil(NLog.LogLevel.Warn); builder.ForLogger("Microsoft.*").WriteToNil(NLog.LogLevel.Warn);
builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn); builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn);
builder
.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel")
.WriteToNil(NLog.LogLevel.Debug);
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget);
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget);

127
StabilityMatrix.Avalonia/Assets.cs

@ -22,39 +22,74 @@ internal static class Assets
public static Uri NoImage { get; } = public static Uri NoImage { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/noimage.png"); new("avares://StabilityMatrix.Avalonia/Assets/noimage.png");
public static AvaloniaResource LicensesJson => new( public static AvaloniaResource LicensesJson =>
"avares://StabilityMatrix.Avalonia/Assets/licenses.json"); new("avares://StabilityMatrix.Avalonia/Assets/licenses.json");
private const UnixFileMode unix755 = UnixFileMode.UserRead | UnixFileMode.UserWrite | private const UnixFileMode unix755 =
UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.UserRead
UnixFileMode.GroupExecute | UnixFileMode.OtherRead | | UnixFileMode.UserWrite
UnixFileMode.OtherExecute; | UnixFileMode.UserExecute
| UnixFileMode.GroupRead
| UnixFileMode.GroupExecute
| UnixFileMode.OtherRead
| UnixFileMode.OtherExecute;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipExecutable => Compat.Switch( public static AvaloniaResource SevenZipExecutable =>
(PlatformKind.Windows, Compat.Switch(
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")), (
(PlatformKind.Linux | PlatformKind.X64, PlatformKind.Windows,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", unix755)), new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")
(PlatformKind.MacOS | PlatformKind.Arm, ),
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", unix755))); (
PlatformKind.Linux | PlatformKind.X64,
new AvaloniaResource(
"avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs",
unix755
)
),
(
PlatformKind.MacOS | PlatformKind.Arm,
new AvaloniaResource(
"avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz",
unix755
)
)
);
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipLicense => Compat.Switch( public static AvaloniaResource SevenZipLicense =>
(PlatformKind.Windows, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt")), Compat.Switch(
(PlatformKind.Linux | PlatformKind.X64, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt")), (
(PlatformKind.MacOS | PlatformKind.Arm, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt"))); PlatformKind.Windows,
new AvaloniaResource(
"avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt"
)
),
(
PlatformKind.Linux | PlatformKind.X64,
new AvaloniaResource(
"avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt"
)
),
(
PlatformKind.MacOS | PlatformKind.Arm,
new AvaloniaResource(
"avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt"
)
)
);
public static AvaloniaResource PyScriptSiteCustomize => new( public static AvaloniaResource PyScriptSiteCustomize =>
"avares://StabilityMatrix.Avalonia/Assets/sitecustomize.py"); new("avares://StabilityMatrix.Avalonia/Assets/sitecustomize.py");
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
public static AvaloniaResource PyScriptGetPip => new( public static AvaloniaResource PyScriptGetPip =>
"avares://StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc"); new("avares://StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc");
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
public static IEnumerable<(AvaloniaResource resource, string relativePath)> PyModuleVenv => public static IEnumerable<(AvaloniaResource resource, string relativePath)> PyModuleVenv =>
@ -63,27 +98,47 @@ internal static class Assets
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")] [SupportedOSPlatform("macos")]
public static RemoteResource PythonDownloadUrl => Compat.Switch( public static RemoteResource PythonDownloadUrl =>
(PlatformKind.Windows | PlatformKind.X64, new RemoteResource( Compat.Switch(
new Uri("https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"), (
"608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d")), PlatformKind.Windows | PlatformKind.X64,
(PlatformKind.Linux | PlatformKind.X64, new RemoteResource( new RemoteResource(
new Uri("https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz"), new Uri(
"c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79")), "https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"
(PlatformKind.MacOS | PlatformKind.Arm, new RemoteResource( ),
new Uri("https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz"), "608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d"
"8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f"))); )
),
(
PlatformKind.Linux | PlatformKind.X64,
new RemoteResource(
new Uri(
"https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz"
),
"c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79"
)
),
(
PlatformKind.MacOS | PlatformKind.Arm,
new RemoteResource(
new Uri(
"https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz"
),
"8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f"
)
)
);
public static Uri DiscordServerUrl { get; } = public static Uri DiscordServerUrl { get; } = new("https://discord.com/invite/TUrgfECxHz");
new("https://discord.com/invite/TUrgfECxHz");
public static Uri PatreonUrl { get; } = public static Uri PatreonUrl { get; } = new("https://patreon.com/StabilityMatrix");
new("https://patreon.com/StabilityMatrix");
/// <summary> /// <summary>
/// Yield AvaloniaResources given a relative directory path within the 'Assets' folder. /// Yield AvaloniaResources given a relative directory path within the 'Assets' folder.
/// </summary> /// </summary>
public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(string relativeAssetPath) public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(
string relativeAssetPath
)
{ {
var baseUri = new Uri("avares://StabilityMatrix.Avalonia/Assets/"); var baseUri = new Uri("avares://StabilityMatrix.Avalonia/Assets/");
var targetUri = new Uri(baseUri, relativeAssetPath); var targetUri = new Uri(baseUri, relativeAssetPath);

72
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -20,7 +20,9 @@ public class BetterContentDialog : ContentDialog
#region Reflection Shenanigans for setting content dialog result #region Reflection Shenanigans for setting content dialog result
[NotNull] [NotNull]
protected static readonly FieldInfo? ResultField = typeof(ContentDialog).GetField( protected static readonly FieldInfo? ResultField = typeof(ContentDialog).GetField(
"_result",BindingFlags.Instance | BindingFlags.NonPublic); "_result",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected ContentDialogResult Result protected ContentDialogResult Result
{ {
@ -30,7 +32,9 @@ public class BetterContentDialog : ContentDialog
[NotNull] [NotNull]
protected static readonly MethodInfo? HideCoreMethod = typeof(ContentDialog).GetMethod( protected static readonly MethodInfo? HideCoreMethod = typeof(ContentDialog).GetMethod(
"HideCore", BindingFlags.Instance | BindingFlags.NonPublic); "HideCore",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected void HideCore() protected void HideCore()
{ {
@ -40,7 +44,9 @@ public class BetterContentDialog : ContentDialog
// Also get button properties to hide on command execution change // Also get button properties to hide on command execution change
[NotNull] [NotNull]
protected static readonly FieldInfo? PrimaryButtonField = typeof(ContentDialog).GetField( protected static readonly FieldInfo? PrimaryButtonField = typeof(ContentDialog).GetField(
"_primaryButton", BindingFlags.Instance | BindingFlags.NonPublic); "_primaryButton",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected Button? PrimaryButton protected Button? PrimaryButton
{ {
@ -50,7 +56,9 @@ public class BetterContentDialog : ContentDialog
[NotNull] [NotNull]
protected static readonly FieldInfo? SecondaryButtonField = typeof(ContentDialog).GetField( protected static readonly FieldInfo? SecondaryButtonField = typeof(ContentDialog).GetField(
"_secondaryButton", BindingFlags.Instance | BindingFlags.NonPublic); "_secondaryButton",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected Button? SecondaryButton protected Button? SecondaryButton
{ {
@ -60,7 +68,9 @@ public class BetterContentDialog : ContentDialog
[NotNull] [NotNull]
protected static readonly FieldInfo? CloseButtonField = typeof(ContentDialog).GetField( protected static readonly FieldInfo? CloseButtonField = typeof(ContentDialog).GetField(
"_closeButton", BindingFlags.Instance | BindingFlags.NonPublic); "_closeButton",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected Button? CloseButton protected Button? CloseButton
{ {
@ -87,8 +97,10 @@ public class BetterContentDialog : ContentDialog
protected override Type StyleKeyOverride { get; } = typeof(ContentDialog); protected override Type StyleKeyOverride { get; } = typeof(ContentDialog);
public static readonly StyledProperty<bool> IsFooterVisibleProperty = AvaloniaProperty.Register<BetterContentDialog, bool>( public static readonly StyledProperty<bool> IsFooterVisibleProperty = AvaloniaProperty.Register<
"IsFooterVisible", true); BetterContentDialog,
bool
>("IsFooterVisible", true);
public bool IsFooterVisible public bool IsFooterVisible
{ {
@ -96,9 +108,11 @@ public class BetterContentDialog : ContentDialog
set => SetValue(IsFooterVisibleProperty, value); set => SetValue(IsFooterVisibleProperty, value);
} }
public static readonly StyledProperty<ScrollBarVisibility> ContentVerticalScrollBarVisibilityProperty public static readonly StyledProperty<ScrollBarVisibility> ContentVerticalScrollBarVisibilityProperty =
= AvaloniaProperty.Register<BetterContentDialog, ScrollBarVisibility>( AvaloniaProperty.Register<BetterContentDialog, ScrollBarVisibility>(
"ContentScrollBarVisibility", ScrollBarVisibility.Auto); "ContentScrollBarVisibility",
ScrollBarVisibility.Auto
);
public ScrollBarVisibility ContentVerticalScrollBarVisibility public ScrollBarVisibility ContentVerticalScrollBarVisibility
{ {
@ -106,8 +120,8 @@ public class BetterContentDialog : ContentDialog
set => SetValue(ContentVerticalScrollBarVisibilityProperty, value); set => SetValue(ContentVerticalScrollBarVisibilityProperty, value);
} }
public static readonly StyledProperty<double> MinDialogWidthProperty = AvaloniaProperty.Register<BetterContentDialog, double>( public static readonly StyledProperty<double> MinDialogWidthProperty =
"MinDialogWidth"); AvaloniaProperty.Register<BetterContentDialog, double>("MinDialogWidth");
public double MinDialogWidth public double MinDialogWidth
{ {
@ -115,8 +129,8 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MinDialogWidthProperty, value); set => SetValue(MinDialogWidthProperty, value);
} }
public static readonly StyledProperty<double> MaxDialogWidthProperty = AvaloniaProperty.Register<BetterContentDialog, double>( public static readonly StyledProperty<double> MaxDialogWidthProperty =
"MaxDialogWidth"); AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogWidth");
public double MaxDialogWidth public double MaxDialogWidth
{ {
@ -124,8 +138,8 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogWidthProperty, value); set => SetValue(MaxDialogWidthProperty, value);
} }
public static readonly StyledProperty<double> MaxDialogHeightProperty = AvaloniaProperty.Register<BetterContentDialog, double>( public static readonly StyledProperty<double> MaxDialogHeightProperty =
"MaxDialogHeight"); AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogHeight");
public double MaxDialogHeight public double MaxDialogHeight
{ {
@ -133,8 +147,8 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogHeightProperty, value); set => SetValue(MaxDialogHeightProperty, value);
} }
public static readonly StyledProperty<Thickness> ContentMarginProperty = AvaloniaProperty.Register<BetterContentDialog, Thickness>( public static readonly StyledProperty<Thickness> ContentMarginProperty =
"ContentMargin"); AvaloniaProperty.Register<BetterContentDialog, Thickness>("ContentMargin");
public Thickness ContentMargin public Thickness ContentMargin
{ {
@ -142,7 +156,6 @@ public class BetterContentDialog : ContentDialog
set => SetValue(ContentMarginProperty, value); set => SetValue(ContentMarginProperty, value);
} }
public BetterContentDialog() public BetterContentDialog()
{ {
AddHandler(LoadedEvent, OnLoaded); AddHandler(LoadedEvent, OnLoaded);
@ -156,6 +169,15 @@ public class BetterContentDialog : ContentDialog
viewModel.SecondaryButtonClick += OnDialogButtonClick; viewModel.SecondaryButtonClick += OnDialogButtonClick;
viewModel.CloseButtonClick += OnDialogButtonClick; viewModel.CloseButtonClick += OnDialogButtonClick;
} }
else if (
(Content as Control)?.DataContext
is ContentDialogProgressViewModelBase progressViewModel
)
{
progressViewModel.PrimaryButtonClick += OnDialogButtonClick;
progressViewModel.SecondaryButtonClick += OnDialogButtonClick;
progressViewModel.CloseButtonClick += OnDialogButtonClick;
}
// If commands provided, bind OnCanExecuteChanged to hide buttons // If commands provided, bind OnCanExecuteChanged to hide buttons
// otherwise link visibility to IsEnabled // otherwise link visibility to IsEnabled
@ -170,7 +192,8 @@ public class BetterContentDialog : ContentDialog
} }
else else
{ {
PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText); PrimaryButton.IsVisible =
IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
} }
} }
@ -185,7 +208,8 @@ public class BetterContentDialog : ContentDialog
} }
else else
{ {
SecondaryButton.IsVisible = IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText); SecondaryButton.IsVisible =
IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText);
} }
} }
@ -254,7 +278,8 @@ public class BetterContentDialog : ContentDialog
var border2 = faBorder?.Child as Border; var border2 = faBorder?.Child as Border;
// Named Grid 'DialogSpace' // Named Grid 'DialogSpace'
if (border2?.Child is not Grid dialogSpaceGrid) throw new InvalidOperationException("Could not find DialogSpace grid"); if (border2?.Child is not Grid dialogSpaceGrid)
throw new InvalidOperationException("Could not find DialogSpace grid");
var scrollViewer = dialogSpaceGrid.Children[0] as ScrollViewer; var scrollViewer = dialogSpaceGrid.Children[0] as ScrollViewer;
var actualBorder = dialogSpaceGrid.Children[1] as Border; var actualBorder = dialogSpaceGrid.Children[1] as Border;
@ -267,7 +292,8 @@ public class BetterContentDialog : ContentDialog
var subBorder = scrollViewer.Content as Border; var subBorder = scrollViewer.Content as Border;
var subGrid = subBorder?.Child as Grid; var subGrid = subBorder?.Child as Grid;
if (subGrid is null) throw new InvalidOperationException("Could not find sub grid"); if (subGrid is null)
throw new InvalidOperationException("Could not find sub grid");
var contentControlTitle = subGrid.Children[0] as ContentControl; var contentControlTitle = subGrid.Children[0] as ContentControl;
// Hide title if empty // Hide title if empty
if (Title is null or string { Length: 0 }) if (Title is null or string { Length: 0 })

147
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -13,6 +13,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -20,6 +21,7 @@ using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
@ -30,7 +32,8 @@ namespace StabilityMatrix.Avalonia.DesignData;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DesignData public static class DesignData
{ {
[NotNull] public static IServiceProvider? Services { get; set; } [NotNull]
public static IServiceProvider? Services { get; set; }
private static bool isInitialized; private static bool isInitialized;
@ -44,7 +47,9 @@ public static class DesignData
var services = new ServiceCollection(); var services = new ServiceCollection();
var activePackageId = Guid.NewGuid(); var activePackageId = Guid.NewGuid();
services.AddSingleton<ISettingsManager, MockSettingsManager>(_ => new MockSettingsManager services.AddSingleton<ISettingsManager, MockSettingsManager>(
_ =>
new MockSettingsManager
{ {
Settings = Settings =
{ {
@ -54,9 +59,11 @@ public static class DesignData
{ {
Id = activePackageId, Id = activePackageId,
DisplayName = "My Installed Package", DisplayName = "My Installed Package",
DisplayVersion = "v1.0.0",
PackageName = "stable-diffusion-webui", PackageName = "stable-diffusion-webui",
PackageVersion = "v1.0.0", Version = new InstalledPackageVersion
{
InstalledReleaseVersion = "v1.0.0"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now LastUpdateCheck = DateTimeOffset.Now
}, },
@ -65,17 +72,24 @@ public static class DesignData
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
DisplayName = "Comfy Diffusion WebUI Dev Branch Long Name", DisplayName = "Comfy Diffusion WebUI Dev Branch Long Name",
PackageName = "ComfyUI", PackageName = "ComfyUI",
DisplayVersion = "main@ab73d4a", Version = new InstalledPackageVersion
{
InstalledBranch = "master",
InstalledCommitSha =
"abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now LastUpdateCheck = DateTimeOffset.Now
} }
}, },
ActiveInstalledPackageId = activePackageId ActiveInstalledPackageId = activePackageId
} }
}); }
);
// General services // General services
services.AddLogging() services
.AddLogging()
.AddSingleton<INavigationService, NavigationService>() .AddSingleton<INavigationService, NavigationService>()
.AddSingleton<IPackageFactory, PackageFactory>() .AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<IUpdateHelper, UpdateHelper>() .AddSingleton<IUpdateHelper, UpdateHelper>()
@ -90,7 +104,8 @@ public static class DesignData
.AddSingleton<IHttpClientFactory, MockHttpClientFactory>() .AddSingleton<IHttpClientFactory, MockHttpClientFactory>()
.AddSingleton<IDiscordRichPresenceService, MockDiscordRichPresenceService>() .AddSingleton<IDiscordRichPresenceService, MockDiscordRichPresenceService>()
.AddSingleton<IModelIndexService, MockModelIndexService>() .AddSingleton<IModelIndexService, MockModelIndexService>()
.AddSingleton<ITrackedDownloadService, MockTrackedDownloadService>(); .AddSingleton<ITrackedDownloadService, MockTrackedDownloadService>()
.AddSingleton<IPackageModificationRunner, PackageModificationRunner>();
// Placeholder services that nobody should need during design time // Placeholder services that nobody should need during design time
services services
@ -122,26 +137,31 @@ public static class DesignData
LaunchOptionsViewModel = Services.GetRequiredService<LaunchOptionsViewModel>(); LaunchOptionsViewModel = Services.GetRequiredService<LaunchOptionsViewModel>();
LaunchOptionsViewModel.Cards = new[] LaunchOptionsViewModel.Cards = new[]
{ {
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition LaunchOptionCard.FromDefinition(
new LaunchOptionDefinition
{ {
Name = "Host", Name = "Host",
Type = LaunchOptionType.String, Type = LaunchOptionType.String,
Description = "The host name for the Web UI", Description = "The host name for the Web UI",
DefaultValue = "localhost", DefaultValue = "localhost",
Options = { "--host" } Options = { "--host" }
}), }
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition ),
LaunchOptionCard.FromDefinition(
new LaunchOptionDefinition
{ {
Name = "API", Name = "API",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
Options = { "--api" } Options = { "--api" }
}) }
)
}; };
LaunchOptionsViewModel.UpdateFilterCards(); LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>(); InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages = InstallerViewModel.AvailablePackages = packageFactory
packageFactory.GetAllAvailablePackages().ToImmutableArray(); .GetAllAvailablePackages()
.ToImmutableArray();
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0]; InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note."; InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
@ -158,8 +178,9 @@ public static class DesignData
{ {
FilePath = "~/Models/StableDiffusion/electricity-light.safetensors", FilePath = "~/Models/StableDiffusion/electricity-light.safetensors",
Title = "Auroral Background", Title = "Auroral Background",
PreviewImagePath = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" + PreviewImagePath =
"78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ConnectedModel = new ConnectedModelInfo ConnectedModel = new ConnectedModelInfo
{ {
VersionName = "Lightning Auroral", VersionName = "Lightning Auroral",
@ -174,11 +195,7 @@ public static class DesignData
} }
} }
}, },
new() new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" },
{
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
},
}, },
}, },
new(settingsManager, downloadService, modelFinder) new(settingsManager, downloadService, modelFinder)
@ -200,11 +217,7 @@ public static class DesignData
}, },
CheckpointFiles = new AdvancedObservableList<CheckpointFile> CheckpointFiles = new AdvancedObservableList<CheckpointFile>
{ {
new() new() { FilePath = "~/Models/Lora/lora_v2.pt", Title = "Best Lora v2", }
{
FilePath = "~/Models/Lora/lora_v2.pt",
Title = "Best Lora v2",
}
} }
} }
}; };
@ -214,8 +227,8 @@ public static class DesignData
folder.DisplayedCheckpointFiles = folder.CheckpointFiles; folder.DisplayedCheckpointFiles = folder.CheckpointFiles;
} }
CheckpointBrowserViewModel.ModelCards = new CheckpointBrowserViewModel.ModelCards =
ObservableCollection<CheckpointBrowserCardViewModel> new ObservableCollection<CheckpointBrowserCardViewModel>
{ {
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm => dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{ {
@ -233,8 +246,9 @@ public static class DesignData
{ {
FilePath = "~/Models/StableDiffusion/electricity-light.safetensors", FilePath = "~/Models/StableDiffusion/electricity-light.safetensors",
Title = "Auroral Background", Title = "Auroral Background",
PreviewImagePath = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" + PreviewImagePath =
"78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ConnectedModel = new ConnectedModelInfo ConnectedModel = new ConnectedModelInfo
{ {
VersionName = "Lightning Auroral", VersionName = "Lightning Auroral",
@ -249,19 +263,28 @@ public static class DesignData
} }
} }
}, },
new() new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }
{
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
}
}; };
ProgressManagerViewModel.ProgressItems.AddRange(new ProgressItemViewModelBase[] ProgressManagerViewModel.ProgressItems.AddRange(
new ProgressItemViewModelBase[]
{ {
new ProgressItemViewModel(new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressItemViewModel(
new ProgressReport(0.5f, "Downloading..."))), new ProgressItem(
Guid.NewGuid(),
"Test File.exe",
new ProgressReport(0.5f, "Downloading...")
)
),
new MockDownloadProgressItemViewModel("Test File 2.exe"), new MockDownloadProgressItemViewModel("Test File 2.exe"),
}); new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
)
}
);
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>(); UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.UpdateText = UpdateViewModel.UpdateText =
@ -272,9 +295,14 @@ public static class DesignData
isInitialized = true; isInitialized = true;
} }
[NotNull] public static InstallerViewModel? InstallerViewModel { get; private set; } [NotNull]
[NotNull] public static LaunchOptionsViewModel? LaunchOptionsViewModel { get; private set; } public static InstallerViewModel? InstallerViewModel { get; private set; }
[NotNull] public static UpdateViewModel? UpdateViewModel { 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>>(); Services.GetRequiredService<ServiceManager<ViewModelBase>>();
@ -296,10 +324,12 @@ public static class DesignData
var vm = Services.GetRequiredService<PackageManagerViewModel>(); var vm = Services.GetRequiredService<PackageManagerViewModel>();
vm.SetPackages(settings.Settings.InstalledPackages); vm.SetPackages(settings.Settings.InstalledPackages);
vm.SetUnknownPackages(new InstalledPackage[] vm.SetUnknownPackages(
new InstalledPackage[]
{ {
UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"),
}); }
);
vm.PackageCards[0].IsUpdateAvailable = true; vm.PackageCards[0].IsUpdateAvailable = true;
@ -328,7 +358,8 @@ public static class DesignData
new() new()
{ {
Name = "BB95 Furry Mix", Name = "BB95 Furry Mix",
Description = @"Introducing SnoutMix Description =
@"Introducing SnoutMix
A Mix of non-Furry and Furry models such as Furtastic and BB95Furry to create a great variety of anthro AI generation options, but bringing out more detail, still giving a lot of freedom to customise the human aspects, and having great backgrounds, with a focus on something more realistic. Works well with realistic character loras. A Mix of non-Furry and Furry models such as Furtastic and BB95Furry to create a great variety of anthro AI generation options, but bringing out more detail, still giving a lot of freedom to customise the human aspects, and having great backgrounds, with a focus on something more realistic. Works well with realistic character loras.
The gallery images are often inpainted, but you will get something very similar if copying their data directly. They are inpainted using the same model, therefore all results are possible without anything custom/hidden-away. Controlnet Tiled is applied to enhance them further afterwards. Gallery images were made with same model but before it was renamed", The gallery images are often inpainted, but you will get something very similar if copying their data directly. They are inpainted using the same model, therefore all results are possible without anything custom/hidden-away. Controlnet Tiled is applied to enhance them further afterwards. Gallery images were made with same model but before it was renamed",
BaseModel = "SD 1.5", BaseModel = "SD 1.5",
@ -355,16 +386,15 @@ The gallery images are often inpainted, but you will get something very similar
Fp = CivitModelFpType.fp32, Fp = CivitModelFpType.fp32,
Size = CivitModelSize.full Size = CivitModelSize.full
}, },
Hashes = new CivitFileHashes Hashes = new CivitFileHashes { BLAKE3 = "ABCD" }
{
BLAKE3 = "ABCD"
}
} }
} }
} }
}; };
var sampleViewModel = var sampleViewModel = new ModelVersionViewModel(
new ModelVersionViewModel(new HashSet<string> {"ABCD"}, sampleCivitVersions[0]); new HashSet<string> { "ABCD" },
sampleCivitVersions[0]
);
// Sample data for dialogs // Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel> { sampleViewModel }; vm.Versions = new List<ModelVersionViewModel> { sampleViewModel };
@ -403,20 +433,15 @@ The gallery images are often inpainted, but you will get something very similar
} }
}); });
public static EnvVarsViewModel EnvVarsViewModel => DialogFactory.Get<EnvVarsViewModel>( public static EnvVarsViewModel EnvVarsViewModel =>
viewModel => DialogFactory.Get<EnvVarsViewModel>(viewModel =>
{ {
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair> viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair> { new("UWU", "TRUE"), };
{
new("UWU", "TRUE"),
};
}); });
public static PackageImportViewModel PackageImportViewModel => public static PackageImportViewModel PackageImportViewModel =>
DialogFactory.Get<PackageImportViewModel>(); DialogFactory.Get<PackageImportViewModel>();
public static RefreshBadgeViewModel RefreshBadgeViewModel => new() public static RefreshBadgeViewModel RefreshBadgeViewModel =>
{ new() { State = ProgressState.Success };
State = ProgressState.Success
};
} }

15
StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs

@ -12,11 +12,16 @@ public class MockLiteDbContext : ILiteDbContext
{ {
public LiteDatabaseAsync Database => throw new NotImplementedException(); public LiteDatabaseAsync Database => throw new NotImplementedException();
public ILiteCollectionAsync<CivitModel> CivitModels => throw new NotImplementedException(); public ILiteCollectionAsync<CivitModel> CivitModels => throw new NotImplementedException();
public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions => throw new NotImplementedException(); public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions =>
public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache => throw new NotImplementedException(); throw new NotImplementedException();
public ILiteCollectionAsync<LocalModelFile> LocalModelFiles => throw new NotImplementedException(); public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache =>
throw new NotImplementedException();
public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3) public ILiteCollectionAsync<LocalModelFile> LocalModelFiles =>
throw new NotImplementedException();
public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(
string hashBlake3
)
{ {
return Task.FromResult<(CivitModel?, CivitModelVersion?)>((null, null)); return Task.FromResult<(CivitModel?, CivitModelVersion?)>((null, null));
} }

4
StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs

@ -21,7 +21,5 @@ public class MockModelIndexService : IModelIndexService
} }
/// <inheritdoc /> /// <inheritdoc />
public void BackgroundRefreshIndex() public void BackgroundRefreshIndex() { }
{
}
} }

60
StabilityMatrix.Avalonia/Program.cs

@ -50,8 +50,10 @@ public class Program
HandleUpdateReplacement(); HandleUpdateReplacement();
var infoVersion = Assembly.GetExecutingAssembly() var infoVersion = Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion; .GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
Compat.AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict); Compat.AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict);
// Configure exception dialog for unhandled exceptions // Configure exception dialog for unhandled exceptions
@ -77,10 +79,7 @@ public class Program
ImageLoader.AsyncImageLoader.Dispose(); ImageLoader.AsyncImageLoader.Dispose();
ImageLoader.AsyncImageLoader = new FallbackRamCachedWebImageLoader(); ImageLoader.AsyncImageLoader = new FallbackRamCachedWebImageLoader();
return AppBuilder.Configure<App>() return AppBuilder.Configure<App>().UsePlatformDetect().WithInterFont().LogToTrace();
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
} }
private static void HandleUpdateReplacement() private static void HandleUpdateReplacement()
@ -93,7 +92,9 @@ public class Program
return; return;
var retryDelays = Backoff.DecorrelatedJitterBackoffV2( var retryDelays = Backoff.DecorrelatedJitterBackoffV2(
TimeSpan.FromMilliseconds(350), retryCount: 5); TimeSpan.FromMilliseconds(350),
retryCount: 5
);
foreach (var delay in retryDelays) foreach (var delay in retryDelays)
{ {
@ -107,11 +108,16 @@ public class Program
// Ensure permissions are set for unix // Ensure permissions are set for unix
if (Compat.IsUnix) if (Compat.IsUnix)
{ {
File.SetUnixFileMode(targetExe, // 0755 File.SetUnixFileMode(
UnixFileMode.UserRead | UnixFileMode.UserWrite | targetExe, // 0755
UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.UserRead
UnixFileMode.GroupExecute | UnixFileMode.OtherRead | | UnixFileMode.UserWrite
UnixFileMode.OtherExecute); | UnixFileMode.UserExecute
| UnixFileMode.GroupRead
| UnixFileMode.GroupExecute
| UnixFileMode.OtherRead
| UnixFileMode.OtherExecute
);
} }
// Start the new app // Start the new app
@ -147,7 +153,8 @@ public class Program
{ {
SentrySdk.Init(o => SentrySdk.Init(o =>
{ {
o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328"; o.Dsn =
"https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328";
o.StackTraceMode = StackTraceMode.Enhanced; o.StackTraceMode = StackTraceMode.Enhanced;
o.TracesSampleRate = 1.0; o.TracesSampleRate = 1.0;
o.IsGlobalModeEnabled = true; o.IsGlobalModeEnabled = true;
@ -161,9 +168,13 @@ public class Program
}); });
} }
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) private static void CurrentDomain_UnhandledException(
object sender,
UnhandledExceptionEventArgs e
)
{ {
if (e.ExceptionObject is not Exception ex) return; if (e.ExceptionObject is not Exception ex)
return;
var logger = LogManager.GetCurrentClassLogger(); var logger = LogManager.GetCurrentClassLogger();
logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message); logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message);
@ -173,14 +184,14 @@ public class Program
SentrySdk.CaptureException(ex); SentrySdk.CaptureException(ex);
} }
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) if (
Application.Current?.ApplicationLifetime
is IClassicDesktopStyleApplicationLifetime lifetime
)
{ {
var dialog = new ExceptionDialog var dialog = new ExceptionDialog
{ {
DataContext = new ExceptionViewModel DataContext = new ExceptionViewModel { Exception = ex }
{
Exception = ex
}
}; };
var mainWindow = lifetime.MainWindow; var mainWindow = lifetime.MainWindow;
@ -195,11 +206,16 @@ public class Program
// https://github.com/AvaloniaUI/Avalonia/issues/4810#issuecomment-704259221 // https://github.com/AvaloniaUI/Avalonia/issues/4810#issuecomment-704259221
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
dialog.ShowDialog(mainWindow).ContinueWith(_ => dialog
.ShowDialog(mainWindow)
.ContinueWith(
_ =>
{ {
cts.Cancel(); cts.Cancel();
ExitWithException(ex); ExitWithException(ex);
}, TaskScheduler.FromCurrentSynchronizationContext()); },
TaskScheduler.FromCurrentSynchronizationContext()
);
Dispatcher.UIThread.MainLoop(cts.Token); Dispatcher.UIThread.MainLoop(cts.Token);
} }

11
StabilityMatrix.Avalonia/ViewModels/Base/ConsoleProgressViewModel.cs

@ -0,0 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public partial class ConsoleProgressViewModel : ProgressViewModel
{
public ConsoleViewModel Console { get; } = new();
[ObservableProperty]
private bool closeWhenFinished;
}

26
StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs

@ -0,0 +1,26 @@
using System;
using FluentAvalonia.UI.Controls;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public class ContentDialogProgressViewModelBase : ConsoleProgressViewModel
{
public event EventHandler<ContentDialogResult>? PrimaryButtonClick;
public event EventHandler<ContentDialogResult>? SecondaryButtonClick;
public event EventHandler<ContentDialogResult>? CloseButtonClick;
public virtual void OnPrimaryButtonClick()
{
PrimaryButtonClick?.Invoke(this, ContentDialogResult.Primary);
}
public virtual void OnSecondaryButtonClick()
{
SecondaryButtonClick?.Invoke(this, ContentDialogResult.Secondary);
}
public virtual void OnCloseButtonClick()
{
CloseButtonClick?.Invoke(this, ContentDialogResult.None);
}
}

3
StabilityMatrix.Avalonia/ViewModels/Dialogs/ContentDialogViewModelBase.cs → StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs

@ -1,8 +1,7 @@
using System; using System;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Base;
public class ContentDialogViewModelBase : ViewModelBase public class ContentDialogViewModelBase : ViewModelBase
{ {

13
StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs

@ -6,11 +6,16 @@ namespace StabilityMatrix.Avalonia.ViewModels.Base;
public abstract partial class ProgressItemViewModelBase : ViewModelBase public abstract partial class ProgressItemViewModelBase : ViewModelBase
{ {
[ObservableProperty] private Guid id; [ObservableProperty]
[ObservableProperty] private string? name; private Guid id;
[ObservableProperty] private bool failed;
[ObservableProperty]
private string? name;
[ObservableProperty]
private bool failed;
public virtual bool IsCompleted => Progress.Value >= 100 || Failed; public virtual bool IsCompleted => Progress.Value >= 100 || Failed;
public ProgressViewModel Progress { get; } = new(); public ContentDialogProgressViewModelBase Progress { get; } = new();
} }

109
StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs

@ -23,14 +23,17 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
// Queue for console updates // Queue for console updates
private BufferBlock<ProcessOutput> buffer = new(); private BufferBlock<ProcessOutput> buffer = new();
// Task that updates the console (runs on UI thread) // Task that updates the console (runs on UI thread)
private Task? updateTask; private Task? updateTask;
// Cancellation token source for updateTask // Cancellation token source for updateTask
private CancellationTokenSource? updateCts; private CancellationTokenSource? updateCts;
public bool IsUpdatesRunning => updateTask?.IsCompleted == false; public bool IsUpdatesRunning => updateTask?.IsCompleted == false;
[ObservableProperty] private TextDocument document = new(); [ObservableProperty]
private TextDocument document = new();
/// <summary> /// <summary>
/// Current offset for write operations. /// Current offset for write operations.
@ -129,7 +132,9 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{ {
using (await writeCursorLock.LockAsync(WriteCursorLockTimeoutToken)) using (await writeCursorLock.LockAsync(WriteCursorLockTimeoutToken))
{ {
Debug.WriteLine($"Reset cursor to end: ({writeCursor} -> {Document.TextLength})"); Logger.ConditionalTrace(
$"Reset cursor to end: ({writeCursor} -> {Document.TextLength})"
);
writeCursor = Document.TextLength; writeCursor = Document.TextLength;
} }
DebugPrintDocument(); DebugPrintDocument();
@ -141,7 +146,8 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
// Get cancellation token // Get cancellation token
var ct = updateCts?.Token var ct =
updateCts?.Token
?? throw new InvalidOperationException("Update cancellation token must be set"); ?? throw new InvalidOperationException("Update cancellation token must be set");
try try
@ -161,15 +167,18 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
} }
var outputType = output.IsStdErr ? "stderr" : "stdout"; var outputType = output.IsStdErr ? "stderr" : "stdout";
Debug.WriteLine($"Processing: [{outputType}] (Text = {output.Text.ToRepr()}, " + Logger.ConditionalTrace(
$"Raw = {output.RawText?.ToRepr()}, " + $"Processing: [{outputType}] (Text = {output.Text.ToRepr()}, "
$"CarriageReturn = {output.CarriageReturn}, " + + $"Raw = {output.RawText?.ToRepr()}, "
$"CursorUp = {output.CursorUp}, " + + $"CarriageReturn = {output.CarriageReturn}, "
$"AnsiCommand = {output.AnsiCommand})"); + $"CursorUp = {output.CursorUp}, "
+ $"AnsiCommand = {output.AnsiCommand})"
);
// Link the cancellation token to the write cursor lock timeout // Link the cancellation token to the write cursor lock timeout
var linkedCt = CancellationTokenSource var linkedCt = CancellationTokenSource
.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken).Token; .CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken)
.Token;
using (await writeCursorLock.LockAsync(linkedCt)) using (await writeCursorLock.LockAsync(linkedCt))
{ {
@ -184,7 +193,10 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
catch (Exception e) catch (Exception e)
{ {
// Log other errors and continue here to not crash the UI thread // Log other errors and continue here to not crash the UI thread
Logger.Error(e, $"Unexpected error in console update loop: {e.GetType().Name} {e.Message}"); Logger.Error(
e,
$"Unexpected error in console update loop: {e.GetType().Name} {e.Message}"
);
} }
} }
@ -222,8 +234,10 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
// See if we need to move the cursor // See if we need to move the cursor
if (lineStartOffset == writeCursor) if (lineStartOffset == writeCursor)
{ {
Debug.WriteLine($"Cursor already at start for carriage return " + Logger.ConditionalTrace(
$"(offset = {lineStartOffset}, line = {currentLine.LineNumber})"); $"Cursor already at start for carriage return "
+ $"(offset = {lineStartOffset}, line = {currentLine.LineNumber})"
);
} }
else else
{ {
@ -233,8 +247,10 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
var lineLength = lineEndOffset - lineStartOffset; var lineLength = lineEndOffset - lineStartOffset;
Document.Remove(lineStartOffset, lineLength); Document.Remove(lineStartOffset, lineLength);
Debug.WriteLine($"Moving cursor to start for carriage return " + Logger.ConditionalTrace(
$"({writeCursor} -> {lineStartOffset})"); $"Moving cursor to start for carriage return "
+ $"({writeCursor} -> {lineStartOffset})"
);
writeCursor = lineStartOffset; writeCursor = lineStartOffset;
} }
} }
@ -254,17 +270,22 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (currentLocation.Line == 1) if (currentLocation.Line == 1)
{ {
// We are already on the first line, ignore // We are already on the first line, ignore
Debug.WriteLine($"Cursor up: Already on first line"); Logger.ConditionalTrace($"Cursor up: Already on first line");
} }
else else
{ {
// We want to move up one line // We want to move up one line
var targetLocation = new TextLocation(currentLocation.Line - 1, currentLocation.Column); var targetLocation = new TextLocation(
currentLocation.Line - 1,
currentLocation.Column
);
var targetOffset = Document.GetOffset(targetLocation); var targetOffset = Document.GetOffset(targetLocation);
// Update cursor to target offset // Update cursor to target offset
Debug.WriteLine($"Cursor up: Moving (line {currentLocation.Line}, {writeCursor})" + Logger.ConditionalTrace(
$" -> (line {targetLocation.Line}, {targetOffset})"); $"Cursor up: Moving (line {currentLocation.Line}, {writeCursor})"
+ $" -> (line {targetLocation.Line}, {targetOffset})"
);
writeCursor = targetOffset; writeCursor = targetOffset;
} }
@ -280,7 +301,9 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
var spaces = new string(' ', currentLine.Length); var spaces = new string(' ', currentLine.Length);
// Insert the text // Insert the text
Debug.WriteLine($"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})"); Logger.ConditionalTrace(
$"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})"
);
using (Document.RunUpdate()) using (Document.RunUpdate())
{ {
Document.Replace(currentLine.Offset, currentLine.Length, spaces); Document.Replace(currentLine.Offset, currentLine.Length, spaces);
@ -322,16 +345,19 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (currentLine.LineNumber < Document.LineCount) if (currentLine.LineNumber < Document.LineCount)
{ {
var nextLine = Document.GetLineByNumber(currentLine.LineNumber + 1); var nextLine = Document.GetLineByNumber(currentLine.LineNumber + 1);
Debug.WriteLine($"Moving cursor to start of next line " + Logger.ConditionalTrace(
$"({writeCursor} -> {nextLine.Offset})"); $"Moving cursor to start of next line "
+ $"({writeCursor} -> {nextLine.Offset})"
);
writeCursor = nextLine.Offset; writeCursor = nextLine.Offset;
} }
else else
{ {
// Otherwise move to end of current line, and direct insert a newline // Otherwise move to end of current line, and direct insert a newline
var lineEndOffset = currentLine.EndOffset; var lineEndOffset = currentLine.EndOffset;
Debug.WriteLine($"Moving cursor to end of current line " + Logger.ConditionalTrace(
$"({writeCursor} -> {lineEndOffset})"); $"Moving cursor to end of current line " + $"({writeCursor} -> {lineEndOffset})"
);
writeCursor = lineEndOffset; writeCursor = lineEndOffset;
DirectWriteToConsole(Environment.NewLine); DirectWriteToConsole(Environment.NewLine);
} }
@ -352,10 +378,11 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (replaceLength > 0) if (replaceLength > 0)
{ {
var newText = text[..replaceLength]; var newText = text[..replaceLength];
Debug.WriteLine( Logger.ConditionalTrace(
$"Replacing: (cursor = {writeCursor}, length = {replaceLength}, " + $"Replacing: (cursor = {writeCursor}, length = {replaceLength}, "
$"text = {Document.GetText(writeCursor, replaceLength).ToRepr()} " + + $"text = {Document.GetText(writeCursor, replaceLength).ToRepr()} "
$"-> {newText.ToRepr()})"); + $"-> {newText.ToRepr()})"
);
Document.Replace(writeCursor, replaceLength, newText); Document.Replace(writeCursor, replaceLength, newText);
writeCursor += replaceLength; writeCursor += replaceLength;
@ -366,8 +393,9 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (remainingLength > 0) if (remainingLength > 0)
{ {
var textToInsert = text[replaceLength..]; var textToInsert = text[replaceLength..];
Debug.WriteLine($"Inserting: (cursor = {writeCursor}, " + Logger.ConditionalTrace(
$"text = {textToInsert.ToRepr()})"); $"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})"
);
Document.Insert(writeCursor, textToInsert); Document.Insert(writeCursor, textToInsert);
writeCursor += textToInsert.Length; writeCursor += textToInsert.Length;
@ -382,6 +410,9 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
[Conditional("DEBUG")] [Conditional("DEBUG")]
private void DebugPrintDocument() private void DebugPrintDocument()
{ {
if (!Logger.IsTraceEnabled)
return;
var text = Document.Text; var text = Document.Text;
// Add a number for each line // Add a number for each line
// Add an arrow line for where the cursor is, for example (cursor on offset 3): // Add an arrow line for where the cursor is, for example (cursor on offset 3):
@ -416,8 +447,8 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
var textWithCursor = string.Join(Environment.NewLine, lines); var textWithCursor = string.Join(Environment.NewLine, lines);
Debug.WriteLine("[Current Document]"); Logger.ConditionalTrace("[Current Document]");
Debug.WriteLine(textWithCursor); Logger.ConditionalTrace(textWithCursor);
} }
/// <summary> /// <summary>
@ -433,6 +464,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
return; return;
} }
// Otherwise, use manual update one // Otherwise, use manual update one
Logger.Debug("Synchronous post update to console: {@Output}", output);
Dispatcher.UIThread.Post(() => ConsoleUpdateOne(output)); Dispatcher.UIThread.Post(() => ConsoleUpdateOne(output));
} }
@ -456,7 +488,8 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
public void Dispose() public void Dispose()
{ {
if (isDisposed) return; if (isDisposed)
return;
updateCts?.Cancel(); updateCts?.Cancel();
updateCts?.Dispose(); updateCts?.Dispose();
@ -466,10 +499,11 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (updateTask is not null) if (updateTask is not null)
{ {
Logger.Debug("Shutting down console update task");
try try
{ {
updateTask.WaitWithoutException( updateTask.WaitWithoutException(new CancellationTokenSource(1000).Token);
new CancellationTokenSource(1000).Token);
updateTask.Dispose(); updateTask.Dispose();
updateTask = null; updateTask = null;
} }
@ -490,7 +524,8 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (isDisposed) return; if (isDisposed)
return;
updateCts?.Cancel(); updateCts?.Cancel();
updateCts?.Dispose(); updateCts?.Dispose();
@ -498,9 +533,13 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (updateTask is not null) if (updateTask is not null)
{ {
Logger.Debug("Waiting for console update task shutdown...");
await updateTask; await updateTask;
updateTask.Dispose(); updateTask.Dispose();
updateTask = null; updateTask = null;
Logger.Debug("Console update task shutdown complete");
} }
isDisposed = true; isDisposed = true;

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

@ -4,6 +4,7 @@ using System.Diagnostics;
using Avalonia.Collections; using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;

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

@ -17,10 +17,12 @@ using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -29,7 +31,6 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
public partial class InstallerViewModel : ContentDialogViewModelBase public partial class InstallerViewModel : ContentDialogViewModelBase
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -40,48 +41,88 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly IPrerequisiteHelper prerequisiteHelper; private readonly IPrerequisiteHelper prerequisiteHelper;
[ObservableProperty] private BasePackage selectedPackage; [ObservableProperty]
[ObservableProperty] private PackageVersion? selectedVersion; private BasePackage selectedPackage;
[ObservableProperty]
private PackageVersion? selectedVersion;
[ObservableProperty]
private IReadOnlyList<BasePackage>? availablePackages;
[ObservableProperty]
private ObservableCollection<GitCommit>? availableCommits;
[ObservableProperty]
private ObservableCollection<PackageVersion>? availableVersions;
[ObservableProperty]
private GitCommit? selectedCommit;
[ObservableProperty]
private string? releaseNotes;
[ObservableProperty] private IReadOnlyList<BasePackage>? availablePackages; [ObservableProperty]
[ObservableProperty] private ObservableCollection<GitCommit>? availableCommits; private string latestVersionText = string.Empty;
[ObservableProperty] private ObservableCollection<PackageVersion>? availableVersions;
[ObservableProperty] private GitCommit? selectedCommit; [ObservableProperty]
private bool isAdvancedMode;
[ObservableProperty] private string? releaseNotes; [ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanInstall))]
private bool showDuplicateWarning;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanInstall))]
private string? installName;
[ObservableProperty]
private SharedFolderMethod selectedSharedFolderMethod;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowTorchVersionOptions))]
private TorchVersion selectedTorchVersion;
// Version types (release or commit) // Version types (release or commit)
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(ReleaseLabelText), [NotifyPropertyChangedFor(
nameof(IsReleaseMode), nameof(SelectedVersion))] nameof(ReleaseLabelText),
nameof(IsReleaseMode),
nameof(SelectedVersion)
)]
private PackageVersionType selectedVersionType = PackageVersionType.Commit; private PackageVersionType selectedVersionType = PackageVersionType.Commit;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))] [NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))]
private PackageVersionType availableVersionTypes = private PackageVersionType availableVersionTypes =
PackageVersionType.GithubRelease | PackageVersionType.Commit; PackageVersionType.GithubRelease | PackageVersionType.Commit;
public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch"; public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch";
public bool IsReleaseMode public bool IsReleaseMode
{ {
get => SelectedVersionType == PackageVersionType.GithubRelease; get => SelectedVersionType == PackageVersionType.GithubRelease;
set => SelectedVersionType = value ? PackageVersionType.GithubRelease : PackageVersionType.Commit; set =>
SelectedVersionType = value
? PackageVersionType.GithubRelease
: PackageVersionType.Commit;
} }
public bool IsReleaseModeAvailable =>
AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease);
public bool ShowTorchVersionOptions => SelectedTorchVersion != TorchVersion.None;
public bool IsReleaseModeAvailable => AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease); public bool CanInstall => !string.IsNullOrWhiteSpace(InstallName) && !ShowDuplicateWarning;
[ObservableProperty] private bool showDuplicateWarning;
[ObservableProperty] private string? installName;
public Base.ProgressViewModel InstallProgress { get; } = new(); public ProgressViewModel InstallProgress { get; } = new();
public IEnumerable<IPackageStep> Steps { get; set; }
public InstallerViewModel( public InstallerViewModel(
ISettingsManager settingsManager, ISettingsManager settingsManager,
IPackageFactory packageFactory, IPackageFactory packageFactory,
IPyRunner pyRunner, IPyRunner pyRunner,
IDownloadService downloadService, INotificationService notificationService, IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) INotificationService notificationService,
IPrerequisiteHelper prerequisiteHelper
)
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.pyRunner = pyRunner; this.pyRunner = pyRunner;
@ -90,40 +131,43 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
this.prerequisiteHelper = prerequisiteHelper; this.prerequisiteHelper = prerequisiteHelper;
// AvailablePackages and SelectedPackage // AvailablePackages and SelectedPackage
AvailablePackages = new ObservableCollection<BasePackage>(packageFactory.GetAllAvailablePackages()); AvailablePackages = new ObservableCollection<BasePackage>(
packageFactory.GetAllAvailablePackages()
);
SelectedPackage = AvailablePackages[0]; SelectedPackage = AvailablePackages[0];
} }
public override void OnLoaded() public override void OnLoaded()
{ {
if (AvailablePackages == null) return; if (AvailablePackages == null)
return;
SelectedPackage = AvailablePackages[0]; SelectedPackage = AvailablePackages[0];
IsReleaseMode = !SelectedPackage.ShouldIgnoreReleases; IsReleaseMode = !SelectedPackage.ShouldIgnoreReleases;
} }
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
{ {
if (Design.IsDesignMode) return; if (Design.IsDesignMode)
return;
// Check for updates // Check for updates
try try
{ {
var versionOptions = await SelectedPackage.GetAllVersionOptions();
if (IsReleaseMode) if (IsReleaseMode)
{ {
var versions = (await SelectedPackage.GetAllVersions()).ToList(); AvailableVersions = new ObservableCollection<PackageVersion>(
AvailableVersions = new ObservableCollection<PackageVersion>(versions); versionOptions.AvailableVersions
if (!AvailableVersions.Any()) return; );
if (!AvailableVersions.Any())
return;
SelectedVersion = AvailableVersions[0]; SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease);
} }
else else
{ {
var branches = (await SelectedPackage.GetAllBranches()).ToList(); AvailableVersions = new ObservableCollection<PackageVersion>(
AvailableVersions = new ObservableCollection<PackageVersion>(branches.Select(b => versionOptions.AvailableBranches
new PackageVersion );
{
TagName = b.Name,
ReleaseNotesMarkdown = b.Commit.Label
}));
UpdateSelectedVersionToLatestMain(); UpdateSelectedVersionToLatestMain();
} }
@ -138,12 +182,12 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
[RelayCommand] [RelayCommand]
private async Task Install() private async Task Install()
{ {
var result = await notificationService.TryAsync(ActuallyInstall(), "Could not install package"); var result = await notificationService.TryAsync(
ActuallyInstall(),
"Could not install package"
);
if (result.IsSuccessful) if (result.IsSuccessful)
{ {
notificationService.Show(new Notification(
$"Package {SelectedPackage.Name} installed successfully!",
"Success", NotificationType.Success));
OnPrimaryButtonClick(); OnPrimaryButtonClick();
} }
else else
@ -161,67 +205,59 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
} }
} }
private async Task ActuallyInstall() private Task ActuallyInstall()
{ {
if (string.IsNullOrWhiteSpace(InstallName)) if (string.IsNullOrWhiteSpace(InstallName))
{ {
notificationService.Show(new Notification("Package name is empty", notificationService.Show(
"Please enter a name for the package", NotificationType.Error)); new Notification(
return; "Package name is empty",
"Please enter a name for the package",
NotificationType.Error
)
);
return Task.CompletedTask;
} }
try var setPackageInstallingStep = new SetPackageInstallingStep(settingsManager, InstallName);
{
await InstallGitIfNecessary();
SelectedPackage.InstallLocation = Path.Combine( var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", InstallName);
settingsManager.LibraryDir, "Packages", InstallName); var prereqStep = new SetupPrerequisitesStep(prerequisiteHelper, pyRunner);
if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled) var downloadOptions = new DownloadPackageVersionOptions();
{ var installedVersion = new InstalledPackageVersion();
InstallProgress.Text = "Installing dependencies...";
InstallProgress.IsIndeterminate = true;
await pyRunner.Initialize();
if (!PyRunner.PipInstalled)
{
await pyRunner.SetupPip();
}
if (!PyRunner.VenvInstalled)
{
await pyRunner.InstallPackage("virtualenv");
}
}
string version;
if (IsReleaseMode) if (IsReleaseMode)
{ {
version = SelectedVersion?.TagName ?? downloadOptions.VersionTag =
throw new NullReferenceException("Selected version is null"); SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
await DownloadPackage(version, false, null); installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag;
} }
else else
{ {
version = SelectedCommit?.Sha ?? downloadOptions.CommitHash =
throw new NullReferenceException("Selected commit is null"); SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null");
installedVersion.InstalledBranch =
await DownloadPackage(version, true, SelectedVersion!.TagName); SelectedVersion?.TagName
} ?? throw new NullReferenceException("Selected version is null");
installedVersion.InstalledCommitSha = downloadOptions.CommitHash;
await InstallPackage(); }
InstallProgress.Text = "Setting up shared folder links..."; var downloadStep = new DownloadPackageVersionStep(
await SelectedPackage.SetupModelFolders(SelectedPackage.InstallLocation); SelectedPackage,
//sharedFolders.SetupLinksForPackage(SelectedPackage, SelectedPackage.InstallLocation); installLocation,
downloadOptions
InstallProgress.Text = "Done"; );
InstallProgress.IsIndeterminate = false; var installStep = new InstallPackageStep(
InstallProgress.Value = 100; SelectedPackage,
EventManager.Instance.OnGlobalProgressChanged(100); SelectedTorchVersion,
installLocation
var branch = SelectedVersionType == PackageVersionType.GithubRelease ? );
null : SelectedVersion!.TagName; var setupModelFoldersStep = new SetupModelFoldersStep(
SelectedPackage,
SelectedSharedFolderMethod,
installLocation
);
var package = new InstalledPackage var package = new InstalledPackage
{ {
@ -229,21 +265,32 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
LibraryPath = Path.Combine("Packages", InstallName), LibraryPath = Path.Combine("Packages", InstallName),
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
PackageName = SelectedPackage.Name, PackageName = SelectedPackage.Name,
PackageVersion = version, Version = installedVersion,
DisplayVersion = GetDisplayVersion(version, branch),
InstalledBranch = branch,
LaunchCommand = SelectedPackage.LaunchCommand, LaunchCommand = SelectedPackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now LastUpdateCheck = DateTimeOffset.Now,
PreferredTorchVersion = SelectedTorchVersion,
PreferredSharedFolderMethod = SelectedSharedFolderMethod
}; };
await using var st = settingsManager.BeginTransaction();
st.Settings.InstalledPackages.Add(package); var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, package);
st.Settings.ActiveInstalledPackageId = package.Id;
} var steps = new List<IPackageStep>
finally
{ {
InstallProgress.Value = 0; setPackageInstallingStep,
InstallProgress.IsIndeterminate = false; prereqStep,
downloadStep,
installStep,
setupModelFoldersStep,
addInstalledPackageStep
};
Steps = steps;
return Task.CompletedTask;
} }
public void Cancel()
{
OnCloseButtonClick();
} }
private void UpdateSelectedVersionToLatestMain() private void UpdateSelectedVersionToLatestMain()
@ -260,57 +307,12 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main"); version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main");
// If still not found, just use the first one // If still not found, just use the first one
version ??= AvailableVersions[0]; version ??= AvailableVersions.FirstOrDefault();
SelectedVersion = version; SelectedVersion = version;
} }
} }
private static string GetDisplayVersion(string version, string? branch)
{
return branch == null ? version : $"{branch}@{version[..7]}";
}
private Task<string> DownloadPackage(string version, bool isCommitHash, string? branch)
{
InstallProgress.Text = "Downloading package...";
var progress = new Progress<ProgressReport>(progress =>
{
InstallProgress.IsIndeterminate = progress.IsIndeterminate;
InstallProgress.Value = progress.Percentage;
EventManager.Instance.OnGlobalProgressChanged((int) progress.Percentage);
});
return SelectedPackage.DownloadPackage(version, isCommitHash, branch, progress);
}
private async Task InstallPackage()
{
InstallProgress.Text = "Installing package...";
SelectedPackage.ConsoleOutput += SelectedPackageOnConsoleOutput;
try
{
var progress = new Progress<ProgressReport>(progress =>
{
InstallProgress.IsIndeterminate = progress.IsIndeterminate;
InstallProgress.Value = progress.Percentage;
EventManager.Instance.OnGlobalProgressChanged((int) progress.Percentage);
});
await SelectedPackage.InstallPackage(progress);
}
finally
{
SelectedPackage.ConsoleOutput -= SelectedPackageOnConsoleOutput;
}
}
private void SelectedPackageOnConsoleOutput(object? sender, ProcessOutput e)
{
InstallProgress.Description = e.Text;
}
[RelayCommand] [RelayCommand]
private async Task ShowPreview() private async Task ShowPreview()
{ {
@ -349,7 +351,8 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
// When changing branch / release modes, refresh // When changing branch / release modes, refresh
// ReSharper disable once UnusedParameterInPartialMethod // ReSharper disable once UnusedParameterInPartialMethod
partial void OnSelectedVersionTypeChanged(PackageVersionType value) => OnSelectedPackageChanged(SelectedPackage); partial void OnSelectedVersionTypeChanged(PackageVersionType value) =>
OnSelectedPackageChanged(SelectedPackage);
partial void OnSelectedPackageChanged(BasePackage value) partial void OnSelectedPackageChanged(BasePackage value)
{ {
@ -361,74 +364,58 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
? PackageVersionType.Commit ? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit; : PackageVersionType.GithubRelease | PackageVersionType.Commit;
if (Design.IsDesignMode) return; SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion();
Dispatcher.UIThread.InvokeAsync(async () => if (Design.IsDesignMode)
return;
Dispatcher.UIThread
.InvokeAsync(async () =>
{ {
Logger.Debug($"Release mode: {IsReleaseMode}"); Logger.Debug($"Release mode: {IsReleaseMode}");
var versions = (await value.GetAllVersions(IsReleaseMode)).ToList(); var versionOptions = await value.GetAllVersionOptions();
AvailableVersions = IsReleaseMode
? new ObservableCollection<PackageVersion>(versionOptions.AvailableVersions)
: new ObservableCollection<PackageVersion>(versionOptions.AvailableBranches);
if (!versions.Any()) return; SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease);
ReleaseNotes = SelectedVersion.ReleaseNotesMarkdown;
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}");
SelectedVersion = AvailableVersions[0];
ReleaseNotes = versions.First().ReleaseNotesMarkdown;
Logger.Debug($"Loaded release notes for {ReleaseNotes}"); Logger.Debug($"Loaded release notes for {ReleaseNotes}");
if (!IsReleaseMode) if (!IsReleaseMode)
{ {
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList(); var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
if (commits is null || commits.Count == 0) return; if (commits is null || commits.Count == 0)
return;
AvailableCommits = new ObservableCollection<GitCommit>(commits); AvailableCommits = new ObservableCollection<GitCommit>(commits);
SelectedCommit = AvailableCommits[0]; SelectedCommit = AvailableCommits.FirstOrDefault();
UpdateSelectedVersionToLatestMain(); UpdateSelectedVersionToLatestMain();
} }
InstallName = SelectedPackage.DisplayName; InstallName = SelectedPackage.DisplayName;
}).SafeFireAndForget(); LatestVersionText = IsReleaseMode
} ? $"Latest version: {SelectedVersion?.TagName}"
: $"Branch: {SelectedVersion?.TagName}";
private async Task InstallGitIfNecessary() })
{ .SafeFireAndForget();
var progressHandler = new Progress<ProgressReport>(progress =>
{
if (progress.Message != null && progress.Message.Contains("Downloading"))
{
InstallProgress.Text = $"Downloading prerequisites... {progress.Percentage:N0}%";
}
else if (progress.Type == ProgressType.Extract)
{
InstallProgress.Text = $"Installing git... {progress.Percentage:N0}%";
}
else if (progress.Title != null && progress.Title.Contains("Unpacking"))
{
InstallProgress.Text = $"Unpacking resources... {progress.Percentage:N0}%";
}
else
{
InstallProgress.Text = progress.Message;
}
InstallProgress.IsIndeterminate = progress.IsIndeterminate;
InstallProgress.Value = Convert.ToInt32(progress.Percentage);
});
await prerequisiteHelper.InstallAllIfNecessary(progressHandler);
} }
partial void OnInstallNameChanged(string? value) partial void OnInstallNameChanged(string? value)
{ {
ShowDuplicateWarning = ShowDuplicateWarning = settingsManager.Settings.InstalledPackages.Any(
settingsManager.Settings.InstalledPackages.Any(p => p => p.LibraryPath == $"Packages{Path.DirectorySeparatorChar}{value}"
p.LibraryPath == $"Packages{Path.DirectorySeparatorChar}{value}"); );
} }
partial void OnSelectedVersionChanged(PackageVersion? value) partial void OnSelectedVersionChanged(PackageVersion? value)
{ {
ReleaseNotes = value?.ReleaseNotesMarkdown ?? string.Empty; ReleaseNotes = value?.ReleaseNotesMarkdown ?? string.Empty;
if (value == null) return; if (value == null)
return;
SelectedCommit = null; SelectedCommit = null;
AvailableCommits?.Clear(); AvailableCommits?.Clear();
@ -440,7 +427,8 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
try try
{ {
var hashes = await SelectedPackage.GetAllCommits(value.TagName); var hashes = await SelectedPackage.GetAllCommits(value.TagName);
if (hashes is null) throw new Exception("No commits found"); if (hashes is null)
throw new Exception("No commits found");
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
@ -452,7 +440,8 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
{ {
Logger.Warn($"Error getting commits: {e.Message}"); Logger.Warn($"Error getting commits: {e.Message}");
} }
}).SafeFireAndForget(); })
.SafeFireAndForget();
} }
} }
} }

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

@ -8,6 +8,7 @@ using System.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using FuzzySharp; using FuzzySharp;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;

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

@ -27,13 +27,26 @@ public partial class OneClickInstallViewModel : ViewModelBase
private readonly ISharedFolders sharedFolders; private readonly ISharedFolders sharedFolders;
private const string DefaultPackageName = "stable-diffusion-webui"; private const string DefaultPackageName = "stable-diffusion-webui";
[ObservableProperty] private string headerText; [ObservableProperty]
[ObservableProperty] private string subHeaderText; private string headerText;
[ObservableProperty] private string subSubHeaderText = string.Empty;
[ObservableProperty] private bool showInstallButton; [ObservableProperty]
[ObservableProperty] private bool isIndeterminate; private string subHeaderText;
[ObservableProperty] private ObservableCollection<BasePackage> allPackages;
[ObservableProperty] private BasePackage selectedPackage; [ObservableProperty]
private string subSubHeaderText = string.Empty;
[ObservableProperty]
private bool showInstallButton;
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
private ObservableCollection<BasePackage> allPackages;
[ObservableProperty]
private BasePackage selectedPackage;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsProgressBarVisible))] [NotifyPropertyChangedFor(nameof(IsProgressBarVisible))]
@ -41,9 +54,14 @@ public partial class OneClickInstallViewModel : ViewModelBase
public bool IsProgressBarVisible => OneClickInstallProgress > 0 || IsIndeterminate; public bool IsProgressBarVisible => OneClickInstallProgress > 0 || IsIndeterminate;
public OneClickInstallViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory, public OneClickInstallViewModel(
IPrerequisiteHelper prerequisiteHelper, ILogger<OneClickInstallViewModel> logger, IPyRunner pyRunner, ISettingsManager settingsManager,
ISharedFolders sharedFolders) IPackageFactory packageFactory,
IPrerequisiteHelper prerequisiteHelper,
ILogger<OneClickInstallViewModel> logger,
IPyRunner pyRunner,
ISharedFolders sharedFolders
)
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
@ -55,9 +73,9 @@ public partial class OneClickInstallViewModel : ViewModelBase
HeaderText = "Welcome to Stability Matrix!"; HeaderText = "Welcome to Stability Matrix!";
SubHeaderText = "Choose your preferred interface and click Install to get started!"; SubHeaderText = "Choose your preferred interface and click Install to get started!";
ShowInstallButton = true; ShowInstallButton = true;
AllPackages = AllPackages = new ObservableCollection<BasePackage>(
new ObservableCollection<BasePackage>(this.packageFactory.GetAllAvailablePackages() this.packageFactory.GetAllAvailablePackages().Where(p => p.OfferInOneClickInstaller)
.Where(p => p.OfferInOneClickInstaller)); );
SelectedPackage = AllPackages[0]; SelectedPackage = AllPackages[0];
} }
@ -107,16 +125,31 @@ public partial class OneClickInstallViewModel : ViewModelBase
// get latest version & download & install // get latest version & download & install
SubHeaderText = "Getting latest version..."; SubHeaderText = "Getting latest version...";
var latestVersion = await SelectedPackage.GetLatestVersion(); var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name);
SelectedPackage.InstallLocation =
Path.Combine(libraryDir, "Packages", SelectedPackage.Name); var downloadVersion = new DownloadPackageVersionOptions();
SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text; var installedVersion = new InstalledPackageVersion();
var versionOptions = await SelectedPackage.GetAllVersionOptions();
if (versionOptions.AvailableVersions != null && versionOptions.AvailableVersions.Any())
{
downloadVersion.VersionTag = versionOptions.AvailableVersions.First().TagName;
installedVersion.InstalledReleaseVersion = downloadVersion.VersionTag;
}
else
{
downloadVersion.BranchName = await SelectedPackage.GetLatestVersion();
installedVersion.InstalledBranch = downloadVersion.BranchName;
}
var torchVersion = SelectedPackage.GetRecommendedTorchVersion();
await DownloadPackage(latestVersion); await DownloadPackage(installLocation, downloadVersion);
await InstallPackage(); await InstallPackage(installLocation, torchVersion);
SubHeaderText = "Setting up shared folder links..."; SubHeaderText = "Setting up shared folder links...";
await SelectedPackage.SetupModelFolders(SelectedPackage.InstallLocation); var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
await SelectedPackage.SetupModelFolders(installLocation, recommendedSharedFolderMethod);
var installedPackage = new InstalledPackage var installedPackage = new InstalledPackage
{ {
@ -124,10 +157,11 @@ public partial class OneClickInstallViewModel : ViewModelBase
LibraryPath = Path.Combine("Packages", SelectedPackage.Name), LibraryPath = Path.Combine("Packages", SelectedPackage.Name),
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
PackageName = SelectedPackage.Name, PackageName = SelectedPackage.Name,
PackageVersion = latestVersion, Version = installedVersion,
DisplayVersion = latestVersion,
LaunchCommand = SelectedPackage.LaunchCommand, LaunchCommand = SelectedPackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now LastUpdateCheck = DateTimeOffset.Now,
PreferredTorchVersion = torchVersion,
PreferredSharedFolderMethod = recommendedSharedFolderMethod
}; };
await using var st = settingsManager.BeginTransaction(); await using var st = settingsManager.BeginTransaction();
st.Settings.InstalledPackages.Add(installedPackage); st.Settings.InstalledPackages.Add(installedPackage);
@ -148,7 +182,10 @@ public partial class OneClickInstallViewModel : ViewModelBase
EventManager.Instance.OnOneClickInstallFinished(false); EventManager.Instance.OnOneClickInstallFinished(false);
} }
private async Task DownloadPackage(string version) private async Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions
)
{ {
SubHeaderText = "Downloading package..."; SubHeaderText = "Downloading package...";
@ -159,14 +196,13 @@ public partial class OneClickInstallViewModel : ViewModelBase
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress); EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
}); });
await SelectedPackage.DownloadPackage(version, false, version, progress); await SelectedPackage.DownloadPackage(installLocation, versionOptions, progress);
SubHeaderText = "Download Complete"; SubHeaderText = "Download Complete";
OneClickInstallProgress = 100; OneClickInstallProgress = 100;
} }
private async Task InstallPackage() private async Task InstallPackage(string installLocation, TorchVersion torchVersion)
{ {
SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text;
SubHeaderText = "Downloading and installing package requirements..."; SubHeaderText = "Downloading and installing package requirements...";
var progress = new Progress<ProgressReport>(progress => var progress = new Progress<ProgressReport>(progress =>
@ -177,6 +213,11 @@ public partial class OneClickInstallViewModel : ViewModelBase
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress); EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
}); });
await SelectedPackage.InstallPackage(progress); await SelectedPackage.InstallPackage(
installLocation,
torchVersion,
progress,
(output) => SubSubHeaderText = output.Text
);
} }
} }

121
StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs

@ -10,6 +10,7 @@ using Avalonia.Controls;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using NLog; using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
@ -29,23 +30,34 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
private readonly IPackageFactory packageFactory; private readonly IPackageFactory packageFactory;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
[ObservableProperty] private DirectoryPath? packagePath; [ObservableProperty]
[ObservableProperty] private BasePackage? selectedBasePackage; private DirectoryPath? packagePath;
[ObservableProperty]
private BasePackage? selectedBasePackage;
public IReadOnlyList<BasePackage> AvailablePackages public IReadOnlyList<BasePackage> AvailablePackages =>
=> packageFactory.GetAllAvailablePackages().ToImmutableArray(); packageFactory.GetAllAvailablePackages().ToImmutableArray();
[ObservableProperty] private PackageVersion? selectedVersion; [ObservableProperty]
private PackageVersion? selectedVersion;
[ObservableProperty] private ObservableCollection<GitCommit>? availableCommits; [ObservableProperty]
[ObservableProperty] private ObservableCollection<PackageVersion>? availableVersions; private ObservableCollection<GitCommit>? availableCommits;
[ObservableProperty]
private ObservableCollection<PackageVersion>? availableVersions;
[ObservableProperty] private GitCommit? selectedCommit; [ObservableProperty]
private GitCommit? selectedCommit;
// Version types (release or commit) // Version types (release or commit)
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(ReleaseLabelText), [NotifyPropertyChangedFor(
nameof(IsReleaseMode), nameof(SelectedVersion))] nameof(ReleaseLabelText),
nameof(IsReleaseMode),
nameof(SelectedVersion)
)]
private PackageVersionType selectedVersionType = PackageVersionType.Commit; private PackageVersionType selectedVersionType = PackageVersionType.Commit;
[ObservableProperty] [ObservableProperty]
@ -56,14 +68,16 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
public bool IsReleaseMode public bool IsReleaseMode
{ {
get => SelectedVersionType == PackageVersionType.GithubRelease; get => SelectedVersionType == PackageVersionType.GithubRelease;
set => SelectedVersionType = value ? PackageVersionType.GithubRelease : PackageVersionType.Commit; set =>
SelectedVersionType = value
? PackageVersionType.GithubRelease
: PackageVersionType.Commit;
} }
public bool IsReleaseModeAvailable => AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease); public bool IsReleaseModeAvailable =>
AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease);
public PackageImportViewModel( public PackageImportViewModel(IPackageFactory packageFactory, ISettingsManager settingsManager)
IPackageFactory packageFactory,
ISettingsManager settingsManager)
{ {
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
@ -73,27 +87,27 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
{ {
SelectedBasePackage ??= AvailablePackages[0]; SelectedBasePackage ??= AvailablePackages[0];
if (Design.IsDesignMode) return; if (Design.IsDesignMode)
return;
// Populate available versions // Populate available versions
try try
{ {
var versionOptions = await SelectedBasePackage.GetAllVersionOptions();
if (IsReleaseMode) if (IsReleaseMode)
{ {
var versions = (await SelectedBasePackage.GetAllVersions()).ToList(); AvailableVersions = new ObservableCollection<PackageVersion>(
AvailableVersions = new ObservableCollection<PackageVersion>(versions); versionOptions.AvailableVersions
if (!AvailableVersions.Any()) return; );
if (!AvailableVersions.Any())
return;
SelectedVersion = AvailableVersions[0]; SelectedVersion = AvailableVersions[0];
} }
else else
{ {
var branches = (await SelectedBasePackage.GetAllBranches()).ToList(); AvailableVersions = new ObservableCollection<PackageVersion>(
AvailableVersions = new ObservableCollection<PackageVersion>(branches.Select(b => versionOptions.AvailableBranches
new PackageVersion );
{
TagName = b.Name,
ReleaseNotesMarkdown = b.Commit.Label
}));
UpdateSelectedVersionToLatestMain(); UpdateSelectedVersionToLatestMain();
} }
} }
@ -119,8 +133,8 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
// When changing branch / release modes, refresh // When changing branch / release modes, refresh
// ReSharper disable once UnusedParameterInPartialMethod // ReSharper disable once UnusedParameterInPartialMethod
partial void OnSelectedVersionTypeChanged(PackageVersionType value) partial void OnSelectedVersionTypeChanged(PackageVersionType value) =>
=> OnSelectedBasePackageChanged(SelectedBasePackage); OnSelectedBasePackageChanged(SelectedBasePackage);
partial void OnSelectedBasePackageChanged(BasePackage? value) partial void OnSelectedBasePackageChanged(BasePackage? value)
{ {
@ -134,33 +148,36 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
AvailableVersions?.Clear(); AvailableVersions?.Clear();
AvailableCommits?.Clear(); AvailableCommits?.Clear();
AvailableVersionTypes = SelectedBasePackage.ShouldIgnoreReleases AvailableVersionTypes = SelectedBasePackage.AvailableVersionTypes;
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
if (Design.IsDesignMode) return; if (Design.IsDesignMode)
return;
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread
.InvokeAsync(async () =>
{ {
Logger.Debug($"Release mode: {IsReleaseMode}"); Logger.Debug($"Release mode: {IsReleaseMode}");
var versions = (await value.GetAllVersions(IsReleaseMode)).ToList(); var versionOptions = await value.GetAllVersionOptions();
if (!versions.Any()) return; AvailableVersions = IsReleaseModeAvailable
? new ObservableCollection<PackageVersion>(versionOptions.AvailableVersions)
: new ObservableCollection<PackageVersion>(versionOptions.AvailableBranches);
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}"); Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}");
SelectedVersion = AvailableVersions[0]; SelectedVersion = AvailableVersions[0];
if (!IsReleaseMode) if (!IsReleaseMode)
{ {
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList(); var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
if (commits is null || commits.Count == 0) return; if (commits is null || commits.Count == 0)
return;
AvailableCommits = new ObservableCollection<GitCommit>(commits); AvailableCommits = new ObservableCollection<GitCommit>(commits);
SelectedCommit = AvailableCommits[0]; SelectedCommit = AvailableCommits[0];
UpdateSelectedVersionToLatestMain(); UpdateSelectedVersionToLatestMain();
} }
}).SafeFireAndForget(); })
.SafeFireAndForget();
} }
private void UpdateSelectedVersionToLatestMain() private void UpdateSelectedVersionToLatestMain()
@ -188,32 +205,35 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
if (SelectedBasePackage is null || PackagePath is null) if (SelectedBasePackage is null || PackagePath is null)
return; return;
string version; var version = new InstalledPackageVersion();
if (IsReleaseMode) if (IsReleaseMode)
{ {
version = SelectedVersion?.TagName ?? version.InstalledReleaseVersion =
throw new NullReferenceException("Selected version is null"); SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
} }
else else
{ {
version = SelectedCommit?.Sha ?? version.InstalledBranch =
throw new NullReferenceException("Selected commit is null"); SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
version.InstalledCommitSha =
SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null");
} }
var branch = SelectedVersionType == PackageVersionType.GithubRelease ? var torchVersion = SelectedBasePackage.GetRecommendedTorchVersion();
null : SelectedVersion!.TagName; var sharedFolderRecommendation = SelectedBasePackage.RecommendedSharedFolderMethod;
var package = new InstalledPackage var package = new InstalledPackage
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
DisplayName = PackagePath.Name, DisplayName = PackagePath.Name,
PackageName = SelectedBasePackage.Name, PackageName = SelectedBasePackage.Name,
LibraryPath = $"Packages{Path.DirectorySeparatorChar}{PackagePath.Name}", LibraryPath = $"Packages{Path.DirectorySeparatorChar}{PackagePath.Name}",
PackageVersion = version, Version = version,
DisplayVersion = GetDisplayVersion(version, branch),
InstalledBranch = branch,
LaunchCommand = SelectedBasePackage.LaunchCommand, LaunchCommand = SelectedBasePackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now, LastUpdateCheck = DateTimeOffset.Now,
PreferredTorchVersion = torchVersion,
PreferredSharedFolderMethod = sharedFolderRecommendation
}; };
// Recreate venv if it's a BaseGitPackage // Recreate venv if it's a BaseGitPackage
@ -223,7 +243,8 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
} }
// Reconfigure shared links // Reconfigure shared links
await SelectedBasePackage.UpdateModelFolders(PackagePath); var recommendedSharedFolderMethod = SelectedBasePackage.RecommendedSharedFolderMethod;
await SelectedBasePackage.UpdateModelFolders(PackagePath, recommendedSharedFolderMethod);
settingsManager.Transaction(s => s.InstalledPackages.Add(package)); settingsManager.Transaction(s => s.InstalledPackages.Add(package));
} }

48
StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs

@ -9,6 +9,7 @@ using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using NLog; using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -30,18 +31,28 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
private const string InvalidDirectoryText = private const string InvalidDirectoryText =
"Directory must be empty or have a valid settings.json file"; "Directory must be empty or have a valid settings.json file";
private const string NotEnoughFreeSpaceText = "Not enough free space on the selected drive"; private const string NotEnoughFreeSpaceText = "Not enough free space on the selected drive";
private const string FatWarningText = private const string FatWarningText = "FAT32 / exFAT drives are not supported at this time";
"FAT32 / exFAT drives are not supported at this time";
[ObservableProperty] private string dataDirectory = DefaultInstallLocation; [ObservableProperty]
[ObservableProperty] private bool isPortableMode; private string dataDirectory = DefaultInstallLocation;
[ObservableProperty] private string directoryStatusText = string.Empty; [ObservableProperty]
[ObservableProperty] private bool isStatusBadgeVisible; private bool isPortableMode;
[ObservableProperty] private bool isDirectoryValid;
[ObservableProperty] private bool showFatWarning;
public RefreshBadgeViewModel ValidatorRefreshBadge { get; } = new() [ObservableProperty]
private string directoryStatusText = string.Empty;
[ObservableProperty]
private bool isStatusBadgeVisible;
[ObservableProperty]
private bool isDirectoryValid;
[ObservableProperty]
private bool showFatWarning;
public RefreshBadgeViewModel ValidatorRefreshBadge { get; } =
new()
{ {
State = ProgressState.Inactive, State = ProgressState.Inactive,
SuccessToolTipText = ValidExistingDirectoryText, SuccessToolTipText = ValidExistingDirectoryText,
@ -89,10 +100,10 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
try try
{ {
var jsonText = await File.ReadAllTextAsync(settingsPath); var jsonText = await File.ReadAllTextAsync(settingsPath);
JsonSerializer.Deserialize<Settings>(jsonText, new JsonSerializerOptions JsonSerializer.Deserialize<Settings>(
{ jsonText,
Converters = { new JsonStringEnumConverter() } new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }
}); );
// If successful, show existing badge // If successful, show existing badge
IsStatusBadgeVisible = true; IsStatusBadgeVisible = true;
IsDirectoryValid = true; IsDirectoryValid = true;
@ -142,13 +153,12 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
private async Task ShowFolderBrowserDialog() private async Task ShowFolderBrowserDialog()
{ {
var provider = App.StorageProvider; var provider = App.StorageProvider;
var result = await provider.OpenFolderPickerAsync(new FolderPickerOpenOptions var result = await provider.OpenFolderPickerAsync(
{ new FolderPickerOpenOptions { Title = "Select Data Folder", AllowMultiple = false }
Title = "Select Data Folder", );
AllowMultiple = false
});
if (result.Count != 1) return; if (result.Count != 1)
return;
DataDirectory = result[0].Path.LocalPath; DataDirectory = result[0].Path.LocalPath;
} }

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

@ -9,6 +9,7 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -23,13 +24,26 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public required string Description { get; set; } public required string Description { get; set; }
public required string Title { get; set; } public required string Title { get; set; }
[ObservableProperty] private Bitmap? previewImage; [ObservableProperty]
[ObservableProperty] private ModelVersionViewModel? selectedVersionViewModel; private Bitmap? previewImage;
[ObservableProperty] private CivitFileViewModel? selectedFile;
[ObservableProperty] private bool isImportEnabled; [ObservableProperty]
[ObservableProperty] private ObservableCollection<ImageSource> imageUrls = new(); private ModelVersionViewModel? selectedVersionViewModel;
[ObservableProperty] private bool canGoToNextImage;
[ObservableProperty] private bool canGoToPreviousImage; [ObservableProperty]
private CivitFileViewModel? selectedFile;
[ObservableProperty]
private bool isImportEnabled;
[ObservableProperty]
private ObservableCollection<ImageSource> imageUrls = new();
[ObservableProperty]
private bool canGoToNextImage;
[ObservableProperty]
private bool canGoToPreviousImage;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayedPageNumber))] [NotifyPropertyChangedFor(nameof(DisplayedPageNumber))]
@ -37,8 +51,10 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public int DisplayedPageNumber => SelectedImageIndex + 1; public int DisplayedPageNumber => SelectedImageIndex + 1;
public SelectModelVersionViewModel(ISettingsManager settingsManager, public SelectModelVersionViewModel(
IDownloadService downloadService) ISettingsManager settingsManager,
IDownloadService downloadService
)
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.downloadService = downloadService; this.downloadService = downloadService;
@ -53,8 +69,10 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value) partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value)
{ {
var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled; var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled;
var allImages = value?.ModelVersion?.Images?.Where( var allImages = value
img => nsfwEnabled || img.Nsfw == "None")?.Select(x => new ImageSource(x.Url)).ToList(); ?.ModelVersion?.Images?.Where(img => nsfwEnabled || img.Nsfw == "None")
?.Select(x => new ImageSource(x.Url))
.ToList();
if (allImages == null || !allImages.Any()) if (allImages == null || !allImages.Any())
{ {
@ -92,14 +110,16 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public void PreviousImage() public void PreviousImage()
{ {
if (SelectedImageIndex > 0) SelectedImageIndex--; if (SelectedImageIndex > 0)
SelectedImageIndex--;
CanGoToPreviousImage = SelectedImageIndex > 0; CanGoToPreviousImage = SelectedImageIndex > 0;
CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1; CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1;
} }
public void NextImage() public void NextImage()
{ {
if (SelectedImageIndex < ImageUrls.Count - 1) SelectedImageIndex++; if (SelectedImageIndex < ImageUrls.Count - 1)
SelectedImageIndex++;
CanGoToPreviousImage = SelectedImageIndex > 0; CanGoToPreviousImage = SelectedImageIndex > 0;
CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1; CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1;
} }

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

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -23,18 +24,29 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
private readonly IHttpClientFactory httpClientFactory; private readonly IHttpClientFactory httpClientFactory;
private readonly IUpdateHelper updateHelper; private readonly IUpdateHelper updateHelper;
[ObservableProperty] private bool isUpdateAvailable; [ObservableProperty]
[ObservableProperty] private UpdateInfo? updateInfo; private bool isUpdateAvailable;
[ObservableProperty] private string? releaseNotes; [ObservableProperty]
[ObservableProperty] private string? updateText; private UpdateInfo? updateInfo;
[ObservableProperty] private int progressValue;
[ObservableProperty] private bool showProgressBar; [ObservableProperty]
private string? releaseNotes;
[ObservableProperty]
private string? updateText;
[ObservableProperty]
private int progressValue;
[ObservableProperty]
private bool showProgressBar;
public UpdateViewModel( public UpdateViewModel(
ISettingsManager settingsManager, ISettingsManager settingsManager,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IUpdateHelper updateHelper) IUpdateHelper updateHelper
)
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.httpClientFactory = httpClientFactory; this.httpClientFactory = httpClientFactory;
@ -50,9 +62,11 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
{ {
if (UpdateInfo is null) return; if (UpdateInfo is null)
return;
UpdateText = $"Stability Matrix v{UpdateInfo.Version} is now available! You currently have v{Compat.AppVersion}. Would you like to update now?"; UpdateText =
$"Stability Matrix v{UpdateInfo.Version} is now available! You currently have v{Compat.AppVersion}. Would you like to update now?";
var client = httpClientFactory.CreateClient(); var client = httpClientFactory.CreateClient();
var response = await client.GetAsync(UpdateInfo.ChangelogUrl); var response = await client.GetAsync(UpdateInfo.ChangelogUrl);
@ -89,19 +103,27 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
ShowProgressBar = true; ShowProgressBar = true;
UpdateText = $"Downloading update v{UpdateInfo.Version}..."; UpdateText = $"Downloading update v{UpdateInfo.Version}...";
await updateHelper.DownloadUpdate(UpdateInfo, new Progress<ProgressReport>(report => await updateHelper.DownloadUpdate(
UpdateInfo,
new Progress<ProgressReport>(report =>
{ {
ProgressValue = Convert.ToInt32(report.Percentage); ProgressValue = Convert.ToInt32(report.Percentage);
})); })
);
// On unix, we need to set the executable bit // On unix, we need to set the executable bit
if (Compat.IsUnix) if (Compat.IsUnix)
{ {
File.SetUnixFileMode(UpdateHelper.ExecutablePath, // 0755 File.SetUnixFileMode(
UnixFileMode.UserRead | UnixFileMode.UserWrite | UpdateHelper.ExecutablePath, // 0755
UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.UserRead
UnixFileMode.GroupExecute | UnixFileMode.OtherRead | | UnixFileMode.UserWrite
UnixFileMode.OtherExecute); | UnixFileMode.UserExecute
| UnixFileMode.GroupRead
| UnixFileMode.GroupExecute
| UnixFileMode.OtherRead
| UnixFileMode.OtherExecute
);
} }
UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds..."; UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds...";

172
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -56,22 +56,34 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
private static partial Regex InputYesNoRegex(); private static partial Regex InputYesNoRegex();
public override string Title => "Launch"; public override string Title => "Launch";
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Rocket, IsFilled = true}; public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Rocket, IsFilled = true };
public ConsoleViewModel Console { get; } = new(); public ConsoleViewModel Console { get; } = new();
[ObservableProperty] private bool launchButtonVisibility; [ObservableProperty]
[ObservableProperty] private bool stopButtonVisibility; private bool launchButtonVisibility;
[ObservableProperty] private bool isLaunchTeachingTipsOpen;
[ObservableProperty] private bool showWebUiButton;
[ObservableProperty, NotifyPropertyChangedFor(nameof(SelectedBasePackage), [ObservableProperty]
nameof(SelectedPackageExtraCommands))] private bool stopButtonVisibility;
[ObservableProperty]
private bool isLaunchTeachingTipsOpen;
[ObservableProperty]
private bool showWebUiButton;
[
ObservableProperty,
NotifyPropertyChangedFor(nameof(SelectedBasePackage), nameof(SelectedPackageExtraCommands))
]
private InstalledPackage? selectedPackage; private InstalledPackage? selectedPackage;
[ObservableProperty] private ObservableCollection<InstalledPackage> installedPackages = new(); [ObservableProperty]
private ObservableCollection<InstalledPackage> installedPackages = new();
[ObservableProperty] private BasePackage? runningPackage; [ObservableProperty]
private BasePackage? runningPackage;
public virtual BasePackage? SelectedBasePackage => public virtual BasePackage? SelectedBasePackage =>
PackageFactory.FindPackageByName(SelectedPackage?.PackageName); PackageFactory.FindPackageByName(SelectedPackage?.PackageName);
@ -83,8 +95,11 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
private string webUiUrl = string.Empty; private string webUiUrl = string.Empty;
// Input info-bars // Input info-bars
[ObservableProperty] private bool showManualInputPrompt; [ObservableProperty]
[ObservableProperty] private bool showConfirmInputPrompt; private bool showManualInputPrompt;
[ObservableProperty]
private bool showConfirmInputPrompt;
public LaunchPageViewModel( public LaunchPageViewModel(
ILogger<LaunchPageViewModel> logger, ILogger<LaunchPageViewModel> logger,
@ -93,7 +108,8 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
IPyRunner pyRunner, IPyRunner pyRunner,
INotificationService notificationService, INotificationService notificationService,
ISharedFolders sharedFolders, ISharedFolders sharedFolders,
ServiceManager<ViewModelBase> dialogFactory) ServiceManager<ViewModelBase> dialogFactory
)
{ {
this.logger = logger; this.logger = logger;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
@ -103,9 +119,11 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
this.sharedFolders = sharedFolders; this.sharedFolders = sharedFolders;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
settingsManager.RelayPropertyFor(this, settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedPackage, vm => vm.SelectedPackage,
settings => settings.ActiveInstalledPackage); settings => settings.ActiveInstalledPackage
);
EventManager.Instance.PackageLaunchRequested += OnPackageLaunchRequested; EventManager.Instance.PackageLaunchRequested += OnPackageLaunchRequested;
EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished; EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished;
@ -130,9 +148,11 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
{ {
if (RunningPackage is not null) if (RunningPackage is not null)
{ {
notificationService.Show("A package is already running", notificationService.Show(
"A package is already running",
"Please stop the current package before launching another.", "Please stop the current package before launching another.",
NotificationType.Error); NotificationType.Error
);
return; return;
} }
@ -143,14 +163,18 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
public override void OnLoaded() public override void OnLoaded()
{ {
// Ensure active package either exists or is null // Ensure active package either exists or is null
settingsManager.Transaction(s => settingsManager.Transaction(
s =>
{ {
s.UpdateActiveInstalledPackage(); s.UpdateActiveInstalledPackage();
}, ignoreMissingLibraryDir: true); },
ignoreMissingLibraryDir: true
);
// Load installed packages // Load installed packages
InstalledPackages = InstalledPackages = new ObservableCollection<InstalledPackage>(
new ObservableCollection<InstalledPackage>(settingsManager.Settings.InstalledPackages); settingsManager.Settings.InstalledPackages
);
// Load active package // Load active package
SelectedPackage = settingsManager.Settings.ActiveInstalledPackage; SelectedPackage = settingsManager.Settings.ActiveInstalledPackage;
@ -170,10 +194,13 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
if (activeInstall == null) if (activeInstall == null)
{ {
// No selected package: error notification // No selected package: error notification
notificationService.Show(new Notification( notificationService.Show(
new Notification(
message: "You must install and select a package before launching", message: "You must install and select a package before launching",
title: "No package selected", title: "No package selected",
type: NotificationType.Error)); type: NotificationType.Error
)
);
return; return;
} }
@ -186,11 +213,16 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
{ {
logger.LogWarning( logger.LogWarning(
"During launch, package name '{PackageName}' did not match a definition", "During launch, package name '{PackageName}' did not match a definition",
activeInstallName); activeInstallName
);
notificationService.Show(new Notification("Package name invalid", notificationService.Show(
new Notification(
"Package name invalid",
"Install package name did not match a definition. Please reinstall and let us know about this issue.", "Install package name did not match a definition. Please reinstall and let us know about this issue.",
NotificationType.Error)); NotificationType.Error
)
);
return; return;
} }
@ -205,12 +237,12 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
.FromDefinitions(definitions, Array.Empty<LaunchOption>()) .FromDefinitions(definitions, Array.Empty<LaunchOption>())
.ToImmutableArray(); .ToImmutableArray();
var args = cards var args = cards.SelectMany(c => c.Options).ToList();
.SelectMany(c => c.Options)
.ToList();
logger.LogDebug("Setting initial launch args: {Args}", logger.LogDebug(
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr()))); "Setting initial launch args: {Args}",
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr()))
);
settingsManager.SaveLaunchArgs(activeInstall.Id, args); settingsManager.SaveLaunchArgs(activeInstall.Id, args);
} }
@ -223,7 +255,6 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Unpack sitecustomize.py to venv // Unpack sitecustomize.py to venv
await UnpackSiteCustomize(packagePath.JoinDir("venv")); await UnpackSiteCustomize(packagePath.JoinDir("venv"));
basePackage.ConsoleOutput += OnProcessOutputReceived;
basePackage.Exited += OnProcessExited; basePackage.Exited += OnProcessExited;
basePackage.StartupComplete += RunningPackageOnStartupComplete; basePackage.StartupComplete += RunningPackageOnStartupComplete;
@ -233,7 +264,10 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
Console.StartUpdates(); Console.StartUpdates();
// Update shared folder links (in case library paths changed) // Update shared folder links (in case library paths changed)
await basePackage.UpdateModelFolders(packagePath); await basePackage.UpdateModelFolders(
packagePath,
activeInstall.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod
);
// Load user launch args from settings and convert to string // Load user launch args from settings and convert to string
var userArgs = settingsManager.GetLaunchArgs(activeInstall.Id); var userArgs = settingsManager.GetLaunchArgs(activeInstall.Id);
@ -245,10 +279,12 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Use input command if provided, otherwise use package launch command // Use input command if provided, otherwise use package launch command
command ??= basePackage.LaunchCommand; command ??= basePackage.LaunchCommand;
await basePackage.RunPackage(packagePath, command, userArgsString); await basePackage.RunPackage(packagePath, command, userArgsString, OnProcessOutputReceived);
RunningPackage = basePackage; RunningPackage = basePackage;
EventManager.Instance.OnRunningPackageStatusChanged(new PackagePair(activeInstall, basePackage)); EventManager.Instance.OnRunningPackageStatusChanged(
new PackagePair(activeInstall, basePackage)
);
} }
// Unpacks sitecustomize.py to the target venv // Unpacks sitecustomize.py to the target venv
@ -292,11 +328,11 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Open a config page // Open a config page
var userLaunchArgs = settingsManager.GetLaunchArgs(activeInstall.Id); var userLaunchArgs = settingsManager.GetLaunchArgs(activeInstall.Id);
var viewModel = dialogFactory.Get<LaunchOptionsViewModel>(); var viewModel = dialogFactory.Get<LaunchOptionsViewModel>();
viewModel.Cards = LaunchOptionCard.FromDefinitions(definitions, userLaunchArgs) viewModel.Cards = LaunchOptionCard
.FromDefinitions(definitions, userLaunchArgs)
.ToImmutableArray(); .ToImmutableArray();
logger.LogDebug("Launching config dialog with cards: {CardsCount}", logger.LogDebug("Launching config dialog with cards: {CardsCount}", viewModel.Cards.Count);
viewModel.Cards.Count);
var dialog = new BetterContentDialog var dialog = new BetterContentDialog
{ {
@ -308,10 +344,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
DefaultButton = ContentDialogButton.Primary, DefaultButton = ContentDialogButton.Primary,
ContentMargin = new Thickness(32, 16), ContentMargin = new Thickness(32, 16),
Padding = new Thickness(0, 16), Padding = new Thickness(0, 16),
Content = new LaunchOptionsDialog Content = new LaunchOptionsDialog { DataContext = viewModel, }
{
DataContext = viewModel,
}
}; };
var result = await dialog.ShowAsync(); var result = await dialog.ShowAsync();
@ -372,7 +405,8 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
public virtual async Task Stop() public virtual async Task Stop()
{ {
if (RunningPackage is null) return; if (RunningPackage is null)
return;
await RunningPackage.WaitForShutdown(); await RunningPackage.WaitForShutdown();
RunningPackage = null; RunningPackage = null;
@ -383,19 +417,27 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
public void OpenWebUi() public void OpenWebUi()
{ {
if (string.IsNullOrEmpty(webUiUrl)) return; if (string.IsNullOrEmpty(webUiUrl))
return;
notificationService.TryAsync(Task.Run(() => ProcessRunner.OpenUrl(webUiUrl)), notificationService.TryAsync(
"Failed to open URL", $"{webUiUrl}"); Task.Run(() => ProcessRunner.OpenUrl(webUiUrl)),
"Failed to open URL",
$"{webUiUrl}"
);
} }
private void OnProcessExited(object? sender, int exitCode) private void OnProcessExited(object? sender, int exitCode)
{ {
EventManager.Instance.OnRunningPackageStatusChanged(null); EventManager.Instance.OnRunningPackageStatusChanged(null);
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread
.InvokeAsync(async () =>
{ {
logger.LogTrace("Process exited ({Code}) at {Time:g}", logger.LogTrace(
exitCode, DateTimeOffset.Now); "Process exited ({Code}) at {Time:g}",
exitCode,
DateTimeOffset.Now
);
// Need to wait for streams to finish before detaching handlers // Need to wait for streams to finish before detaching handlers
if (sender is BaseGitPackage { VenvRunner: not null } package) if (sender is BaseGitPackage { VenvRunner: not null } package)
@ -411,7 +453,10 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
} }
catch (OperationCanceledException e) catch (OperationCanceledException e)
{ {
logger.LogWarning("Waiting for process EOF timed out: {Message}", e.Message); logger.LogWarning(
"Waiting for process EOF timed out: {Message}",
e.Message
);
} }
} }
} }
@ -419,7 +464,6 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Detach handlers // Detach handlers
if (sender is BasePackage basePackage) if (sender is BasePackage basePackage)
{ {
basePackage.ConsoleOutput -= OnProcessOutputReceived;
basePackage.Exited -= OnProcessExited; basePackage.Exited -= OnProcessExited;
basePackage.StartupComplete -= RunningPackageOnStartupComplete; basePackage.StartupComplete -= RunningPackageOnStartupComplete;
} }
@ -431,13 +475,15 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Need to reset cursor in case its in some weird position // Need to reset cursor in case its in some weird position
// from progress bars // from progress bars
await Console.ResetWriteCursor(); await Console.ResetWriteCursor();
Console.PostLine($"{Environment.NewLine}Process finished with exit code {exitCode}"); Console.PostLine(
$"{Environment.NewLine}Process finished with exit code {exitCode}"
}).SafeFireAndForget(); );
})
.SafeFireAndForget();
} }
// Callback for processes // Callback for processes
private void OnProcessOutputReceived(object? sender, ProcessOutput output) private void OnProcessOutputReceived(ProcessOutput output)
{ {
Console.Post(output); Console.Post(output);
EventManager.Instance.OnScrollToBottomRequested(); EventManager.Instance.OnScrollToBottomRequested();
@ -464,23 +510,29 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
e.Cancel = true; e.Cancel = true;
var dialog = CreateExitConfirmDialog(); var dialog = CreateExitConfirmDialog();
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher.UIThread
.InvokeAsync(async () =>
{ {
if ((TaskDialogStandardResult) if (
await dialog.ShowAsync(true) == TaskDialogStandardResult.Yes) (TaskDialogStandardResult)await dialog.ShowAsync(true)
== TaskDialogStandardResult.Yes
)
{ {
App.Services.GetRequiredService<MainWindow>().Hide(); App.Services.GetRequiredService<MainWindow>().Hide();
App.Shutdown(); App.Shutdown();
} }
}).SafeFireAndForget(); })
.SafeFireAndForget();
} }
} }
} }
private static TaskDialog CreateExitConfirmDialog() private static TaskDialog CreateExitConfirmDialog()
{ {
var dialog = DialogHelper.CreateTaskDialog("Confirm Exit", var dialog = DialogHelper.CreateTaskDialog(
"Are you sure you want to exit? This will also close the currently running package."); "Confirm Exit",
"Are you sure you want to exit? This will also close the currently running package."
);
dialog.ShowProgressBar = false; dialog.ShowProgressBar = false;
dialog.FooterVisibility = TaskDialogFooterVisibility.Never; dialog.FooterVisibility = TaskDialogFooterVisibility.Never;

28
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -11,6 +11,7 @@ using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
@ -47,7 +48,8 @@ public partial class MainWindowViewModel : ViewModelBase
ISettingsManager settingsManager, ISettingsManager settingsManager,
IDiscordRichPresenceService discordRichPresenceService, IDiscordRichPresenceService discordRichPresenceService,
ServiceManager<ViewModelBase> dialogFactory, ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService) ITrackedDownloadService trackedDownloadService
)
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
@ -72,7 +74,8 @@ public partial class MainWindowViewModel : ViewModelBase
await base.OnLoadedAsync(); await base.OnLoadedAsync();
// Skip if design mode // Skip if design mode
if (Design.IsDesignMode) return; if (Design.IsDesignMode)
return;
if (!await EnsureDataDirectory()) if (!await EnsureDataDirectory())
{ {
@ -98,16 +101,14 @@ public partial class MainWindowViewModel : ViewModelBase
IsPrimaryButtonEnabled = false, IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false, IsSecondaryButtonEnabled = false,
IsFooterVisible = false, IsFooterVisible = false,
Content = new OneClickInstallDialog Content = new OneClickInstallDialog { DataContext = viewModel },
{
DataContext = viewModel
},
}; };
EventManager.Instance.OneClickInstallFinished += (_, skipped) => EventManager.Instance.OneClickInstallFinished += (_, skipped) =>
{ {
dialog.Hide(); dialog.Hide();
if (skipped) return; if (skipped)
return;
EventManager.Instance.OnTeachingTooltipNeeded(); EventManager.Instance.OnTeachingTooltipNeeded();
}; };
@ -125,7 +126,8 @@ public partial class MainWindowViewModel : ViewModelBase
if (!settingsManager.TryFindLibrary()) if (!settingsManager.TryFindLibrary())
{ {
var result = await ShowSelectDataDirectoryDialog(); var result = await ShowSelectDataDirectoryDialog();
if (!result) return false; if (!result)
return false;
} }
// Try to find library again, should be found now // Try to find library again, should be found now
@ -155,10 +157,7 @@ public partial class MainWindowViewModel : ViewModelBase
IsPrimaryButtonEnabled = false, IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false, IsSecondaryButtonEnabled = false,
IsFooterVisible = false, IsFooterVisible = false,
Content = new SelectDataDirectoryDialog Content = new SelectDataDirectoryDialog { DataContext = viewModel }
{
DataContext = viewModel
}
}; };
var result = await dialog.ShowAsync(); var result = await dialog.ShowAsync();
@ -191,10 +190,7 @@ public partial class MainWindowViewModel : ViewModelBase
IsPrimaryButtonEnabled = false, IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false, IsSecondaryButtonEnabled = false,
IsFooterVisible = false, IsFooterVisible = false,
Content = new UpdateDialog Content = new UpdateDialog { DataContext = viewModel }
{
DataContext = viewModel
}
}; };
await dialog.ShowAsync(); await dialog.ShowAsync();

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

@ -34,11 +34,20 @@ public partial class PackageCardViewModel : ProgressViewModel
private readonly INavigationService navigationService; private readonly INavigationService navigationService;
private readonly ServiceManager<ViewModelBase> vmFactory; private readonly ServiceManager<ViewModelBase> vmFactory;
[ObservableProperty] private InstalledPackage? package; [ObservableProperty]
[ObservableProperty] private string? cardImageSource; private InstalledPackage? package;
[ObservableProperty] private bool isUpdateAvailable;
[ObservableProperty] private string? installedVersion; [ObservableProperty]
[ObservableProperty] private bool isUnknownPackage; private string? cardImageSource;
[ObservableProperty]
private bool isUpdateAvailable;
[ObservableProperty]
private string? installedVersion;
[ObservableProperty]
private bool isUnknownPackage;
public PackageCardViewModel( public PackageCardViewModel(
ILogger<PackageCardViewModel> logger, ILogger<PackageCardViewModel> logger,
@ -46,7 +55,8 @@ public partial class PackageCardViewModel : ProgressViewModel
INotificationService notificationService, INotificationService notificationService,
ISettingsManager settingsManager, ISettingsManager settingsManager,
INavigationService navigationService, INavigationService navigationService,
ServiceManager<ViewModelBase> vmFactory) ServiceManager<ViewModelBase> vmFactory
)
{ {
this.logger = logger; this.logger = logger;
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
@ -72,9 +82,8 @@ public partial class PackageCardViewModel : ProgressViewModel
IsUnknownPackage = false; IsUnknownPackage = false;
var basePackage = packageFactory[value.PackageName]; var basePackage = packageFactory[value.PackageName];
CardImageSource = basePackage?.PreviewImageUri.ToString() CardImageSource = basePackage?.PreviewImageUri.ToString() ?? Assets.NoImage.ToString();
?? Assets.NoImage.ToString(); InstalledVersion = value.Version?.DisplayVersion ?? "Unknown";
InstalledVersion = value.DisplayVersion ?? "Unknown";
} }
} }
@ -104,7 +113,8 @@ public partial class PackageCardViewModel : ProgressViewModel
var dialog = new ContentDialog var dialog = new ContentDialog
{ {
Title = "Are you sure?", 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.", 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", PrimaryButtonText = "Yes, delete it",
CloseButtonText = "No, keep it", CloseButtonText = "No, keep it",
DefaultButton = ContentDialogButton.Primary DefaultButton = ContentDialogButton.Primary
@ -120,13 +130,19 @@ public partial class PackageCardViewModel : ProgressViewModel
var packagePath = new DirectoryPath(settingsManager.LibraryDir, Package.LibraryPath); var packagePath = new DirectoryPath(settingsManager.LibraryDir, Package.LibraryPath);
var deleteTask = packagePath.DeleteVerboseAsync(logger); var deleteTask = packagePath.DeleteVerboseAsync(logger);
var taskResult = await notificationService.TryAsync(deleteTask, var taskResult = await notificationService.TryAsync(
"Some files could not be deleted. Please close any open files in the package directory and try again."); deleteTask,
"Some files could not be deleted. Please close any open files in the package directory and try again."
);
if (taskResult.IsSuccessful) if (taskResult.IsSuccessful)
{ {
notificationService.Show(new Notification("Success", notificationService.Show(
new Notification(
"Success",
$"Package {Package.DisplayName} uninstalled", $"Package {Package.DisplayName} uninstalled",
NotificationType.Success)); NotificationType.Success
)
);
if (!IsUnknownPackage) if (!IsUnknownPackage)
{ {
@ -143,16 +159,21 @@ public partial class PackageCardViewModel : ProgressViewModel
public async Task Update() public async Task Update()
{ {
if (Package is null || IsUnknownPackage) return; if (Package is null || IsUnknownPackage)
return;
var basePackage = packageFactory[Package.PackageName!]; var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null) if (basePackage == null)
{ {
logger.LogWarning("Could not find package {SelectedPackagePackageName}", logger.LogWarning(
Package.PackageName); "Could not find package {SelectedPackagePackageName}",
notificationService.Show("Invalid Package type", Package.PackageName
);
notificationService.Show(
"Invalid Package type",
$"Package {Package.PackageName.ToRepr()} is not a valid package type", $"Package {Package.PackageName.ToRepr()} is not a valid package type",
NotificationType.Error); NotificationType.Error
);
return; return;
} }
@ -162,14 +183,16 @@ public partial class PackageCardViewModel : ProgressViewModel
IsIndeterminate = true; IsIndeterminate = true;
var progressId = Guid.NewGuid(); var progressId = Guid.NewGuid();
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, EventManager.Instance.OnProgressChanged(
new ProgressItem(
progressId,
Package.DisplayName ?? Package.PackageName!, Package.DisplayName ?? Package.PackageName!,
new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update))); new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update)
)
);
try try
{ {
basePackage.InstallLocation = Package.FullPath!;
var progress = new Progress<ProgressReport>(progress => var progress = new Progress<ProgressReport>(progress =>
{ {
var percent = Convert.ToInt32(progress.Percentage); var percent = Convert.ToInt32(progress.Percentage);
@ -179,35 +202,54 @@ public partial class PackageCardViewModel : ProgressViewModel
Text = $"Updating {Package.DisplayName}"; Text = $"Updating {Package.DisplayName}";
EventManager.Instance.OnGlobalProgressChanged(percent); EventManager.Instance.OnGlobalProgressChanged(percent);
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, EventManager.Instance.OnProgressChanged(
packageName, progress)); new ProgressItem(progressId, packageName, progress)
);
}); });
var updateResult = await basePackage.Update(Package, progress); var torchVersion =
Package.PreferredTorchVersion ?? basePackage.GetRecommendedTorchVersion();
var updateResult = await basePackage.Update(Package, torchVersion, progress);
settingsManager.UpdatePackageVersionNumber(Package.Id, updateResult); settingsManager.UpdatePackageVersionNumber(Package.Id, updateResult);
notificationService.Show("Update complete", notificationService.Show(
"Update complete",
$"{Package.DisplayName} has been updated to the latest version.", $"{Package.DisplayName} has been updated to the latest version.",
NotificationType.Success); NotificationType.Success
);
await using (settingsManager.BeginTransaction()) await using (settingsManager.BeginTransaction())
{ {
Package.UpdateAvailable = false; Package.UpdateAvailable = false;
} }
IsUpdateAvailable = false; IsUpdateAvailable = false;
InstalledVersion = Package.DisplayVersion ?? "Unknown"; InstalledVersion = updateResult.DisplayVersion ?? "Unknown";
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, EventManager.Instance.OnProgressChanged(
new ProgressItem(
progressId,
packageName, packageName,
new ProgressReport(1f, "Update complete", type: ProgressType.Update))); new ProgressReport(1f, "Update complete", type: ProgressType.Update)
)
);
} }
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, "Error Updating Package ({PackageName})", basePackage.Name); logger.LogError(e, "Error Updating Package ({PackageName})", basePackage.Name);
notificationService.ShowPersistent($"Error Updating {Package.DisplayName}", e.Message, NotificationType.Error); notificationService.ShowPersistent(
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, $"Error Updating {Package.DisplayName}",
e.Message,
NotificationType.Error
);
EventManager.Instance.OnProgressChanged(
new ProgressItem(
progressId,
packageName, packageName,
new ProgressReport(0f, "Update failed", type: ProgressType.Update), Failed: true)); new ProgressReport(0f, "Update failed", type: ProgressType.Update),
Failed: true
)
);
} }
finally finally
{ {
@ -219,27 +261,23 @@ public partial class PackageCardViewModel : ProgressViewModel
public async Task Import() public async Task Import()
{ {
if (!IsUnknownPackage || Design.IsDesignMode) return; if (!IsUnknownPackage || Design.IsDesignMode)
return;
var viewModel = vmFactory.Get<PackageImportViewModel>(vm => var viewModel = vmFactory.Get<PackageImportViewModel>(vm =>
{ {
vm.PackagePath = vm.PackagePath = new DirectoryPath(
new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException()); Package?.FullPath ?? throw new InvalidOperationException()
);
}); });
var dialog = new TaskDialog var dialog = new TaskDialog
{ {
Content = new PackageImportDialog Content = new PackageImportDialog { DataContext = viewModel },
{
DataContext = viewModel
},
ShowProgressBar = false, ShowProgressBar = false,
Buttons = new List<TaskDialogButton> Buttons = new List<TaskDialogButton>
{ {
new(Resources.Action_Import, TaskDialogStandardResult.Yes) new(Resources.Action_Import, TaskDialogStandardResult.Yes) { IsDefault = true },
{
IsDefault = true
},
new(Resources.Action_Cancel, TaskDialogStandardResult.Cancel) new(Resources.Action_Cancel, TaskDialogStandardResult.Cancel)
} }
}; };
@ -256,7 +294,9 @@ public partial class PackageCardViewModel : ProgressViewModel
await using (new MinimumDelay(200, 300)) await using (new MinimumDelay(200, 300))
{ {
var result = await notificationService.TryAsync(viewModel.AddPackageWithCurrentInputs()); var result = await notificationService.TryAsync(
viewModel.AddPackageWithCurrentInputs()
);
if (result.IsSuccessful) if (result.IsSuccessful)
{ {
EventManager.Instance.OnInstalledPackagesChanged(); EventManager.Instance.OnInstalledPackagesChanged();
@ -289,8 +329,9 @@ public partial class PackageCardViewModel : ProgressViewModel
if (basePackage == null) if (basePackage == null)
return false; return false;
var canCheckUpdate = Package.LastUpdateCheck == null || var canCheckUpdate =
Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15); Package.LastUpdateCheck == null
|| Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15);
if (!canCheckUpdate) if (!canCheckUpdate)
{ {

49
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@ -21,6 +22,7 @@ using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -37,6 +39,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory; private readonly IPackageFactory packageFactory;
private readonly ServiceManager<ViewModelBase> dialogFactory; private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly INotificationService notificationService;
public override string Title => "Packages"; public override string Title => "Packages";
public override IconSource IconSource => public override IconSource IconSource =>
@ -61,12 +64,14 @@ public partial class PackageManagerViewModel : PageViewModelBase
public PackageManagerViewModel( public PackageManagerViewModel(
ISettingsManager settingsManager, ISettingsManager settingsManager,
IPackageFactory packageFactory, IPackageFactory packageFactory,
ServiceManager<ViewModelBase> dialogFactory ServiceManager<ViewModelBase> dialogFactory,
INotificationService notificationService
) )
{ {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
this.notificationService = notificationService;
EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged; EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
@ -77,11 +82,14 @@ public partial class PackageManagerViewModel : PageViewModelBase
.Or(unknown) .Or(unknown)
.DeferUntilLoaded() .DeferUntilLoaded()
.Bind(Packages) .Bind(Packages)
.Transform(p => dialogFactory.Get<PackageCardViewModel>(vm => .Transform(
p =>
dialogFactory.Get<PackageCardViewModel>(vm =>
{ {
vm.Package = p; vm.Package = p;
vm.OnLoadedAsync().SafeFireAndForget(); vm.OnLoadedAsync().SafeFireAndForget();
})) })
)
.Bind(PackageCards) .Bind(PackageCards)
.Subscribe(); .Subscribe();
} }
@ -101,7 +109,10 @@ public partial class PackageManagerViewModel : PageViewModelBase
if (Design.IsDesignMode) if (Design.IsDesignMode)
return; return;
installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer); installedPackages.EditDiff(
settingsManager.Settings.InstalledPackages,
InstalledPackage.Comparer
);
var currentUnknown = await Task.Run(IndexUnknownPackages); var currentUnknown = await Task.Run(IndexUnknownPackages);
unknownInstalledPackages.Edit(s => s.Load(currentUnknown)); unknownInstalledPackages.Edit(s => s.Load(currentUnknown));
@ -115,7 +126,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
var dialog = new BetterContentDialog var dialog = new BetterContentDialog
{ {
MaxDialogWidth = 1100, MaxDialogWidth = 900,
MinDialogWidth = 900, MinDialogWidth = 900,
DefaultButton = ContentDialogButton.Close, DefaultButton = ContentDialogButton.Close,
IsPrimaryButtonEnabled = false, IsPrimaryButtonEnabled = false,
@ -124,8 +135,21 @@ public partial class PackageManagerViewModel : PageViewModelBase
Content = new InstallerDialog { DataContext = viewModel } Content = new InstallerDialog { DataContext = viewModel }
}; };
await dialog.ShowAsync(); var result = await dialog.ShowAsync();
await OnLoadedAsync(); if (result == ContentDialogResult.Primary)
{
var runner = new PackageModificationRunner();
var steps = viewModel.Steps;
EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps.ToList());
EventManager.Instance.OnInstalledPackagesChanged();
notificationService.Show(
"Package Install Complete",
$"{viewModel.InstallName} installed successfully",
NotificationType.Success
);
}
} }
private IEnumerable<UnknownInstalledPackage> IndexUnknownPackages() private IEnumerable<UnknownInstalledPackage> IndexUnknownPackages()
@ -139,9 +163,11 @@ public partial class PackageManagerViewModel : PageViewModelBase
var currentPackages = settingsManager.Settings.InstalledPackages.ToImmutableArray(); var currentPackages = settingsManager.Settings.InstalledPackages.ToImmutableArray();
foreach (var subDir in packageDir.Info foreach (
var subDir in packageDir.Info
.EnumerateDirectories() .EnumerateDirectories()
.Select(info => new DirectoryPath(info))) .Select(info => new DirectoryPath(info))
)
{ {
var expectedLibraryPath = $"Packages{Path.DirectorySeparatorChar}{subDir.Name}"; var expectedLibraryPath = $"Packages{Path.DirectorySeparatorChar}{subDir.Name}";
@ -151,6 +177,11 @@ public partial class PackageManagerViewModel : PageViewModelBase
continue; continue;
} }
if (settingsManager.PackageInstallsInProgress.Contains(subDir.Name))
{
continue;
}
yield return UnknownInstalledPackage.FromDirectoryName(subDir.Name); yield return UnknownInstalledPackage.FromDirectoryName(subDir.Name);
} }
} }

2
StabilityMatrix.Avalonia/ViewModels/DownloadProgressItemViewModel.cs → StabilityMatrix.Avalonia/ViewModels/Progress/DownloadProgressItemViewModel.cs

@ -4,7 +4,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.Progress;
public class DownloadProgressItemViewModel : PausableProgressItemViewModelBase public class DownloadProgressItemViewModel : PausableProgressItemViewModelBase
{ {

78
StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs

@ -0,0 +1,78 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Avalonia.ViewModels.Progress;
public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase
{
private readonly IPackageModificationRunner packageModificationRunner;
private BetterContentDialog? dialog;
public PackageInstallProgressItemViewModel(IPackageModificationRunner packageModificationRunner)
{
this.packageModificationRunner = packageModificationRunner;
Id = packageModificationRunner.Id;
Name = packageModificationRunner.CurrentStep?.ProgressTitle;
Progress.Value = packageModificationRunner.CurrentProgress.Percentage;
Progress.Text = packageModificationRunner.ConsoleOutput.LastOrDefault();
Progress.IsIndeterminate = packageModificationRunner.CurrentProgress.IsIndeterminate;
Progress.Console.StartUpdates();
Progress.Console.Post(
string.Join(Environment.NewLine, packageModificationRunner.ConsoleOutput)
);
packageModificationRunner.ProgressChanged += PackageModificationRunnerOnProgressChanged;
}
private void PackageModificationRunnerOnProgressChanged(object? sender, ProgressReport e)
{
Progress.Value = e.Percentage;
Progress.Description = e.Message;
Progress.IsIndeterminate = e.IsIndeterminate;
Progress.Text = packageModificationRunner.CurrentStep?.ProgressTitle;
Name = packageModificationRunner.CurrentStep?.ProgressTitle;
if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Contains("Downloading..."))
return;
Progress.Console.PostLine(e.Message);
EventManager.Instance.OnScrollToBottomRequested();
if (
e is { Message: not null, Percentage: >= 100 }
&& e.Message.Contains("Package Install Complete")
&& Progress.CloseWhenFinished
)
{
Dispatcher.UIThread.Post(() => dialog?.Hide());
}
}
public async Task ShowProgressDialog()
{
Progress.CloseWhenFinished = true;
dialog = new BetterContentDialog
{
MaxDialogWidth = 900,
MinDialogWidth = 900,
DefaultButton = ContentDialogButton.Close,
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new PackageModificationDialog { DataContext = Progress }
};
EventManager.Instance.OnToggleProgressFlyout();
await dialog.ShowAsync();
}
}

6
StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs → StabilityMatrix.Avalonia/ViewModels/Progress/ProgressItemViewModel.cs

@ -1,10 +1,8 @@
using System; using StabilityMatrix.Avalonia.ViewModels.Base;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.Progress;
public class ProgressItemViewModel : ProgressItemViewModelBase public class ProgressItemViewModel : ProgressItemViewModelBase
{ {

56
StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs → StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs

@ -1,8 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
@ -10,12 +13,13 @@ using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.Progress;
[View(typeof(ProgressManagerPage))] [View(typeof(ProgressManagerPage))]
public partial class ProgressManagerViewModel : PageViewModelBase public partial class ProgressManagerViewModel : PageViewModelBase
@ -23,18 +27,32 @@ public partial class ProgressManagerViewModel : PageViewModelBase
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
public override string Title => "Download Manager"; public override string Title => "Download Manager";
public override IconSource IconSource => new SymbolIconSource {Symbol = Symbol.ArrowCircleDown, IsFilled = true}; public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.ArrowCircleDown, IsFilled = true };
public AvaloniaList<ProgressItemViewModelBase> ProgressItems { get; } = new(); public AvaloniaList<ProgressItemViewModelBase> ProgressItems { get; } = new();
[ObservableProperty]
private bool isOpen;
public ProgressManagerViewModel( public ProgressManagerViewModel(
ITrackedDownloadService trackedDownloadService, ITrackedDownloadService trackedDownloadService,
INotificationService notificationService) INotificationService notificationService
)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
// Attach to the event // Attach to the event
trackedDownloadService.DownloadAdded += TrackedDownloadService_OnDownloadAdded; trackedDownloadService.DownloadAdded += TrackedDownloadService_OnDownloadAdded;
EventManager.Instance.PackageInstallProgressAdded += InstanceOnPackageInstallProgressAdded;
EventManager.Instance.ToggleProgressFlyout += (_, _) => IsOpen = !IsOpen;
}
private void InstanceOnPackageInstallProgressAdded(
object? sender,
IPackageModificationRunner runner
)
{
AddPackageInstall(runner).SafeFireAndForget();
} }
private void TrackedDownloadService_OnDownloadAdded(object? sender, TrackedDownload e) private void TrackedDownloadService_OnDownloadAdded(object? sender, TrackedDownload e)
@ -51,7 +69,11 @@ public partial class ProgressManagerViewModel : PageViewModelBase
case ProgressState.Success: case ProgressState.Success:
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
notificationService.Show("Download Completed", $"Download of {e.FileName} completed successfully.", NotificationType.Success); notificationService.Show(
"Download Completed",
$"Download of {e.FileName} completed successfully.",
NotificationType.Success
);
}); });
break; break;
case ProgressState.Failed: case ProgressState.Failed:
@ -62,13 +84,21 @@ public partial class ProgressManagerViewModel : PageViewModelBase
} }
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
notificationService.ShowPersistent("Download Failed", $"Download of {e.FileName} failed: {msg}", NotificationType.Error); notificationService.ShowPersistent(
"Download Failed",
$"Download of {e.FileName} failed: {msg}",
NotificationType.Error
);
}); });
break; break;
case ProgressState.Cancelled: case ProgressState.Cancelled:
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
notificationService.Show("Download Cancelled", $"Download of {e.FileName} was cancelled.", NotificationType.Warning); notificationService.Show(
"Download Cancelled",
$"Download of {e.FileName} was cancelled.",
NotificationType.Warning
);
}); });
break; break;
} }
@ -90,6 +120,18 @@ public partial class ProgressManagerViewModel : PageViewModelBase
} }
} }
private async Task AddPackageInstall(IPackageModificationRunner packageModificationRunner)
{
if (ProgressItems.Any(vm => vm.Id == packageModificationRunner.Id))
{
return;
}
var vm = new PackageInstallProgressItemViewModel(packageModificationRunner);
ProgressItems.Add(vm);
await vm.ShowProgressDialog();
}
private void ShowFailedNotification(string title, string message) private void ShowFailedNotification(string title, string message)
{ {
notificationService.ShowPersistent(title, message, NotificationType.Error); notificationService.ShowPersistent(title, message, NotificationType.Error);

218
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -55,58 +55,59 @@ public partial class SettingsViewModel : PageViewModelBase
public SharedState SharedState { get; } public SharedState SharedState { get; }
public override string Title => "Settings"; public override string Title => "Settings";
public override IconSource IconSource => new SymbolIconSource {Symbol = Symbol.Settings, IsFilled = true}; public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
// ReSharper disable once MemberCanBeMadeStatic.Global // ReSharper disable once MemberCanBeMadeStatic.Global
public string AppVersion => $"Version {Compat.AppVersion}" + public string AppVersion =>
(Program.IsDebugBuild ? " (Debug)" : ""); $"Version {Compat.AppVersion}" + (Program.IsDebugBuild ? " (Debug)" : "");
// Theme section // Theme section
[ObservableProperty] private string? selectedTheme; [ObservableProperty]
private string? selectedTheme;
public IReadOnlyList<string> AvailableThemes { get; } = new[] public IReadOnlyList<string> AvailableThemes { get; } = new[] { "Light", "Dark", "System", };
{
"Light",
"Dark",
"System",
};
[ObservableProperty] private CultureInfo selectedLanguage; [ObservableProperty]
private CultureInfo selectedLanguage;
// ReSharper disable once MemberCanBeMadeStatic.Global // ReSharper disable once MemberCanBeMadeStatic.Global
public IReadOnlyList<CultureInfo> AvailableLanguages => Cultures.SupportedCultures; public IReadOnlyList<CultureInfo> AvailableLanguages => Cultures.SupportedCultures;
public IReadOnlyList<float> AnimationScaleOptions { get; } = new[] public IReadOnlyList<float> AnimationScaleOptions { get; } =
{ new[] { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f, };
0f,
0.25f,
0.5f,
0.75f,
1f,
1.25f,
1.5f,
1.75f,
2f,
};
[ObservableProperty] private float selectedAnimationScale; [ObservableProperty]
private float selectedAnimationScale;
// Shared folder options // Shared folder options
[ObservableProperty] private bool removeSymlinksOnShutdown; [ObservableProperty]
private bool removeSymlinksOnShutdown;
// Integrations section // Integrations section
[ObservableProperty] private bool isDiscordRichPresenceEnabled; [ObservableProperty]
private bool isDiscordRichPresenceEnabled;
// Debug section // Debug section
[ObservableProperty] private string? debugPaths; [ObservableProperty]
[ObservableProperty] private string? debugCompatInfo; private string? debugPaths;
[ObservableProperty] private string? debugGpuInfo;
[ObservableProperty]
private string? debugCompatInfo;
[ObservableProperty]
private string? debugGpuInfo;
// Info section // Info section
private const int VersionTapCountThreshold = 7; private const int VersionTapCountThreshold = 7;
[ObservableProperty, NotifyPropertyChangedFor(nameof(VersionFlyoutText))] private int versionTapCount;
[ObservableProperty] private bool isVersionTapTeachingTipOpen; [ObservableProperty, NotifyPropertyChangedFor(nameof(VersionFlyoutText))]
public string VersionFlyoutText => $"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options."; private int versionTapCount;
[ObservableProperty]
private bool isVersionTapTeachingTipOpen;
public string VersionFlyoutText =>
$"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options.";
public SettingsViewModel( public SettingsViewModel(
INotificationService notificationService, INotificationService notificationService,
@ -116,7 +117,8 @@ public partial class SettingsViewModel : PageViewModelBase
ServiceManager<ViewModelBase> dialogFactory, ServiceManager<ViewModelBase> dialogFactory,
SharedState sharedState, SharedState sharedState,
ITrackedDownloadService trackedDownloadService, ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService) IModelIndexService modelIndexService
)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
@ -133,23 +135,26 @@ public partial class SettingsViewModel : PageViewModelBase
RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown; RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown;
SelectedAnimationScale = settingsManager.Settings.AnimationScale; SelectedAnimationScale = settingsManager.Settings.AnimationScale;
settingsManager.RelayPropertyFor(this, settingsManager.RelayPropertyFor(this, vm => vm.SelectedTheme, settings => settings.Theme);
vm => vm.SelectedTheme,
settings => settings.Theme);
settingsManager.RelayPropertyFor(this, settingsManager.RelayPropertyFor(
this,
vm => vm.IsDiscordRichPresenceEnabled, vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled); settings => settings.IsDiscordRichPresenceEnabled
);
settingsManager.RelayPropertyFor(this, settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedAnimationScale, vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale); settings => settings.AnimationScale
);
} }
partial void OnSelectedThemeChanged(string? value) partial void OnSelectedThemeChanged(string? value)
{ {
// In case design / tests // In case design / tests
if (Application.Current is null) return; if (Application.Current is null)
return;
// Change theme // Change theme
Application.Current.RequestedThemeVariant = value switch Application.Current.RequestedThemeVariant = value switch
{ {
@ -161,12 +166,12 @@ public partial class SettingsViewModel : PageViewModelBase
partial void OnSelectedLanguageChanged(CultureInfo? oldValue, CultureInfo newValue) partial void OnSelectedLanguageChanged(CultureInfo? oldValue, CultureInfo newValue)
{ {
if (oldValue is null || newValue.Name == Cultures.Current.Name) return; if (oldValue is null || newValue.Name == Cultures.Current.Name)
return;
// Set locale // Set locale
if (AvailableLanguages.Contains(newValue)) if (AvailableLanguages.Contains(newValue))
{ {
Logger.Info("Changing language from {Old} to {New}", Logger.Info("Changing language from {Old} to {New}", oldValue, newValue);
oldValue, newValue);
Cultures.TrySetSupportedCulture(newValue); Cultures.TrySetSupportedCulture(newValue);
settingsManager.Transaction(s => s.Language = newValue.Name); settingsManager.Transaction(s => s.Language = newValue.Name);
@ -191,8 +196,11 @@ public partial class SettingsViewModel : PageViewModelBase
} }
else else
{ {
Logger.Info("Requested invalid language change from {Old} to {New}", Logger.Info(
oldValue, newValue); "Requested invalid language change from {Old} to {New}",
oldValue,
newValue
);
} }
} }
@ -205,8 +213,11 @@ public partial class SettingsViewModel : PageViewModelBase
{ {
settingsManager.Transaction(s => s.InstalledModelHashes = new HashSet<string>()); settingsManager.Transaction(s => s.InstalledModelHashes = new HashSet<string>());
await Task.Run(() => settingsManager.IndexCheckpoints()); await Task.Run(() => settingsManager.IndexCheckpoints());
notificationService.Show("Checkpoint cache reset", "The checkpoint cache has been reset.", notificationService.Show(
NotificationType.Success); "Checkpoint cache reset",
"The checkpoint cache has been reset.",
NotificationType.Success
);
} }
#region Package Environment #region Package Environment
@ -217,17 +228,15 @@ public partial class SettingsViewModel : PageViewModelBase
var viewModel = dialogFactory.Get<EnvVarsViewModel>(); var viewModel = dialogFactory.Get<EnvVarsViewModel>();
// Load current settings // Load current settings
var current = settingsManager.Settings.EnvironmentVariables var current =
?? new Dictionary<string, string>(); settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>( viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>(
current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value))); current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value))
);
var dialog = new BetterContentDialog var dialog = new BetterContentDialog
{ {
Content = new EnvVarsDialog Content = new EnvVarsDialog { DataContext = viewModel },
{
DataContext = viewModel
},
PrimaryButtonText = "Save", PrimaryButtonText = "Save",
IsPrimaryButtonEnabled = true, IsPrimaryButtonEnabled = true,
CloseButtonText = "Cancel", CloseButtonText = "Cancel",
@ -288,8 +297,7 @@ public partial class SettingsViewModel : PageViewModelBase
{ {
if (!Compat.IsWindows) if (!Compat.IsWindows)
{ {
notificationService.Show( notificationService.Show("Not supported", "This feature is only supported on Windows.");
"Not supported", "This feature is only supported on Windows.");
return; return;
} }
@ -297,18 +305,21 @@ public partial class SettingsViewModel : PageViewModelBase
var shortcutDir = new DirectoryPath( var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
"Programs"); "Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk"); var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath; var appPath = Compat.AppCurrentPath;
var iconPath = shortcutDir.JoinFile("Stability Matrix.ico"); var iconPath = shortcutDir.JoinFile("Stability Matrix.ico");
await Assets.AppIcon.ExtractTo(iconPath); await Assets.AppIcon.ExtractTo(iconPath);
WindowsShortcuts.CreateShortcut( WindowsShortcuts.CreateShortcut(shortcutLink, appPath, iconPath, "Stability Matrix");
shortcutLink, appPath, iconPath, "Stability Matrix");
notificationService.Show("Added to Start Menu", notificationService.Show(
"Stability Matrix has been added to the Start Menu.", NotificationType.Success); "Added to Start Menu",
"Stability Matrix has been added to the Start Menu.",
NotificationType.Success
);
} }
/// <summary> /// <summary>
@ -320,15 +331,15 @@ public partial class SettingsViewModel : PageViewModelBase
{ {
if (!Compat.IsWindows) if (!Compat.IsWindows)
{ {
notificationService.Show( notificationService.Show("Not supported", "This feature is only supported on Windows.");
"Not supported", "This feature is only supported on Windows.");
return; return;
} }
// Confirmation dialog // Confirmation dialog
var dialog = new BetterContentDialog var dialog = new BetterContentDialog
{ {
Title = "This will create a shortcut for Stability Matrix in the Start Menu for all users", Title =
"This will create a shortcut for Stability Matrix in the Start Menu for all users",
Content = "You will be prompted for administrator privileges. Continue?", Content = "You will be prompted for administrator privileges. Continue?",
PrimaryButtonText = "Yes", PrimaryButtonText = "Yes",
CloseButtonText = "Cancel", CloseButtonText = "Cancel",
@ -344,7 +355,8 @@ public partial class SettingsViewModel : PageViewModelBase
var shortcutDir = new DirectoryPath( var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu), Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs"); "Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk"); var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath; var appPath = Compat.AppCurrentPath;
@ -355,19 +367,26 @@ public partial class SettingsViewModel : PageViewModelBase
await Assets.AppIcon.ExtractTo(tempDir.JoinFile("Stability Matrix.ico")); await Assets.AppIcon.ExtractTo(tempDir.JoinFile("Stability Matrix.ico"));
WindowsShortcuts.CreateShortcut( WindowsShortcuts.CreateShortcut(
tempDir.JoinFile("Stability Matrix.lnk"), appPath, iconPath, tempDir.JoinFile("Stability Matrix.lnk"),
"Stability Matrix"); appPath,
iconPath,
"Stability Matrix"
);
// Move to target // Move to target
try try
{ {
var moveLinkResult = await WindowsElevated.MoveFiles( var moveLinkResult = await WindowsElevated.MoveFiles(
(tempDir.JoinFile("Stability Matrix.lnk"), shortcutLink), (tempDir.JoinFile("Stability Matrix.lnk"), shortcutLink),
(tempDir.JoinFile("Stability Matrix.ico"), iconPath)); (tempDir.JoinFile("Stability Matrix.ico"), iconPath)
);
if (moveLinkResult != 0) if (moveLinkResult != 0)
{ {
notificationService.ShowPersistent("Failed to create shortcut", $"Could not copy shortcut", notificationService.ShowPersistent(
NotificationType.Error); "Failed to create shortcut",
$"Could not copy shortcut",
NotificationType.Error
);
} }
} }
catch (Win32Exception e) catch (Win32Exception e)
@ -378,8 +397,11 @@ public partial class SettingsViewModel : PageViewModelBase
return; return;
} }
notificationService.Show("Added to Start Menu", notificationService.Show(
"Stability Matrix has been added to the Start Menu for all users.", NotificationType.Success); "Added to Start Menu",
"Stability Matrix has been added to the Start Menu for all users.",
NotificationType.Success
);
} }
public async Task PickNewDataDirectory() public async Task PickNewDataDirectory()
@ -390,10 +412,7 @@ public partial class SettingsViewModel : PageViewModelBase
IsPrimaryButtonEnabled = false, IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false, IsSecondaryButtonEnabled = false,
IsFooterVisible = false, IsFooterVisible = false,
Content = new SelectDataDirectoryDialog Content = new SelectDataDirectoryDialog { DataContext = viewModel }
{
DataContext = viewModel
}
}; };
var result = await dialog.ShowAsync(); var result = await dialog.ShowAsync();
@ -475,10 +494,13 @@ public partial class SettingsViewModel : PageViewModelBase
[RelayCommand] [RelayCommand]
private void DebugNotification() private void DebugNotification()
{ {
notificationService.Show(new Notification( notificationService.Show(
new Notification(
title: "Test Notification", title: "Test Notification",
message: "Here is some message", message: "Here is some message",
type: NotificationType.Information)); type: NotificationType.Information
)
);
} }
[RelayCommand] [RelayCommand]
@ -493,8 +515,7 @@ public partial class SettingsViewModel : PageViewModelBase
}; };
var result = await dialog.ShowAsync(); var result = await dialog.ShowAsync();
notificationService.Show(new Notification("Content dialog closed", notificationService.Show(new Notification("Content dialog closed", $"Result: {result}"));
$"Result: {result}"));
} }
[RelayCommand] [RelayCommand]
@ -515,14 +536,8 @@ public partial class SettingsViewModel : PageViewModelBase
{ {
var textFields = new TextBoxField[] var textFields = new TextBoxField[]
{ {
new() new() { Label = "Url", },
{ new() { Label = "File path" }
Label = "Url",
},
new()
{
Label = "File path"
}
}; };
var dialog = DialogHelper.CreateTextEntryDialog("Add download", "", textFields); var dialog = DialogHelper.CreateTextEntryDialog("Add download", "", textFields);
@ -542,7 +557,8 @@ public partial class SettingsViewModel : PageViewModelBase
public void OnVersionClick() public void OnVersionClick()
{ {
// Ignore if already enabled // Ignore if already enabled
if (SharedState.IsDebugMode) return; if (SharedState.IsDebugMode)
return;
VersionTapCount++; VersionTapCount++;
@ -555,7 +571,9 @@ public partial class SettingsViewModel : PageViewModelBase
// Enable debug options // Enable debug options
SharedState.IsDebugMode = true; SharedState.IsDebugMode = true;
notificationService.Show( notificationService.Show(
"Debug options enabled", "Warning: Improper use may corrupt application state or cause loss of data."); "Debug options enabled",
"Warning: Improper use may corrupt application state or cause loss of data."
);
VersionTapCount = 0; VersionTapCount = 0;
break; break;
} }
@ -579,8 +597,11 @@ public partial class SettingsViewModel : PageViewModelBase
} }
catch (Exception e) catch (Exception e)
{ {
notificationService.Show("Failed to read licenses information", notificationService.Show(
$"{e}", NotificationType.Error); "Failed to read licenses information",
$"{e}",
NotificationType.Error
);
} }
} }
@ -588,15 +609,17 @@ public partial class SettingsViewModel : PageViewModelBase
{ {
// Read licenses.json // Read licenses.json
using var reader = new StreamReader(Assets.LicensesJson.Open()); using var reader = new StreamReader(Assets.LicensesJson.Open());
var licenses = JsonSerializer var licenses =
.Deserialize<IReadOnlyList<LicenseInfo>>(reader.ReadToEnd()) ?? JsonSerializer.Deserialize<IReadOnlyList<LicenseInfo>>(reader.ReadToEnd())
throw new InvalidOperationException("Failed to read licenses.json"); ?? throw new InvalidOperationException("Failed to read licenses.json");
// Generate markdown // Generate markdown
var builder = new StringBuilder(); var builder = new StringBuilder();
foreach (var license in licenses) foreach (var license in licenses)
{ {
builder.AppendLine($"## [{license.PackageName}]({license.PackageUrl}) by {string.Join(", ", license.Authors)}"); builder.AppendLine(
$"## [{license.PackageName}]({license.PackageUrl}) by {string.Join(", ", license.Authors)}"
);
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(license.Description); builder.AppendLine(license.Description);
builder.AppendLine(); builder.AppendLine();
@ -608,5 +631,4 @@ public partial class SettingsViewModel : PageViewModelBase
} }
#endregion #endregion
} }

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

@ -8,134 +8,94 @@
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs" xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core" xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
x:DataType="dialogs:InstallerViewModel" x:DataType="dialogs:InstallerViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="500" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700"
d:DataContext="{x:Static mocks:DesignData.InstallerViewModel}" d:DataContext="{x:Static mocks:DesignData.InstallerViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.InstallerDialog"> x:Class="StabilityMatrix.Avalonia.Views.Dialogs.InstallerDialog">
<Grid RowDefinitions="Auto,Auto,Auto,*"> <Grid MaxHeight="900"
<!-- Close button --> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"
<Grid HorizontalAlignment="Right"> ColumnDefinitions="*,Auto">
<Button <!-- Title and image -->
Margin="0,8,8,0" <Grid Grid.Row="0" Grid.Column="0"
Classes="transparent" Margin="16, 0, 0, 0"
IsEnabled="{Binding !InstallProgress.IsProgressVisible}" RowDefinitions="Auto, Auto, Auto, Auto, Auto">
Command="{Binding OnCloseButtonClick}" <TextBlock Grid.Row="0" Text="{Binding SelectedPackage.DisplayName}"
icons:Attached.Icon="fa-solid fa-xmark"/> FontSize="24"
Margin="0, 16, 0, 4" />
<TextBlock Grid.Row="1" Text="{Binding SelectedPackage.Blurb}"
Margin="0, 0, 0, 4" />
<TextBlock Grid.Row="2" Text="{Binding SelectedPackage.Disclaimer}"
Margin="0, 0, 0, 4"
TextWrapping="Wrap"
Foreground="{DynamicResource ThemeRedColor}"
IsVisible="{Binding SelectedPackage.Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<controls:BetterAdvancedImage Grid.Row="4"
Margin="0, 12,0,0"
Source="{Binding SelectedPackage.PreviewImageUri}"
HorizontalAlignment="Center"
MaxHeight="300"
MaxWidth="600"
Stretch="UniformToFill" />
</Grid> </Grid>
<StackPanel
Grid.Row="1"
Margin="16,8,16,0"
Orientation="Vertical"
DataContext="{Binding InstallProgress}"
IsVisible="{Binding IsProgressVisible}">
<TextBlock <Grid Grid.Column="0" Grid.Row="2"
Grid.ColumnSpan="2"
Margin="16,0,16,0"
RowDefinitions="Auto, Auto, Auto, Auto"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Padding="8" ColumnDefinitions="Auto, Auto, Auto, Auto">
Text="{Binding Text}" />
<ProgressBar <Label Grid.Row="1" Grid.Column="0"
IsIndeterminate="{Binding IsIndeterminate}" Margin="8,0"
Maximum="100" Content="Display Name" />
Width="500" <TextBox Grid.Row="2" Grid.Column="0"
Value="{Binding Value}" /> MinWidth="250"
<TextBlock Margin="8,0"
FontSize="10" VerticalAlignment="Stretch"
HorizontalAlignment="Center" VerticalContentAlignment="Center"
Padding="4" Text="{Binding InstallName, Mode=TwoWay}" />
Text="{Binding Description}"
TextWrapping="Wrap" />
</StackPanel>
<Grid Grid.Row="2" HorizontalAlignment="Left" ColumnDefinitions="Auto,*,Auto">
<ListBox
Margin="8,16"
IsEnabled="{Binding !InstallProgress.IsProgressVisible}"
ItemsSource="{Binding AvailablePackages}"
SelectedItem="{Binding SelectedPackage, Mode=TwoWay}">
<!--<ListView.Style>
<Style TargetType="ListView">
<Setter Property="Background" Value="#191919" />
</Style>
</ListView.Style>-->
<ListBox.Template>
<ControlTemplate>
<!-- BorderBrush="{KeyboardFocusBorderColorBrush}" -->
<Border
BorderThickness="1"
CornerRadius="5">
<ItemsPresenter />
</Border>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type packages:BasePackage}">
<StackPanel Margin="8" VerticalAlignment="Top">
<TextBlock Margin="0,5,0,5" Text="{Binding DisplayName}" />
<TextBlock Margin="0,0,0,5" Text="{Binding ByAuthor}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel <StackPanel Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="4"
MinWidth="400" Orientation="Horizontal"
Grid.Column="1" Margin="8"
Margin="8,16,0,16" IsVisible="{Binding ShowDuplicateWarning}">
Orientation="Vertical"> <ui:SymbolIcon
Foreground="{DynamicResource ThemeRedColor}"
Margin="0,0,8,0"
Symbol="Alert" />
<TextBlock <TextBlock
FontSize="24" Foreground="{DynamicResource ThemeRedColor}"
FontWeight="Bold" TextAlignment="Left"
Text="{Binding SelectedPackage.DisplayName, FallbackValue=Stable Diffusion Web UI}" /> TextWrapping="Wrap">
<TextBlock FontSize="12" Text="{Binding SelectedPackage.ByAuthor, FallbackValue=by Automatic111}" /> <Run Text="An installation with this name already exists." />
<LineBreak />
<Button <Run Text="Please choose a different name or select a different Install Location." />
Background="Transparent"
BorderBrush="Transparent"
Command="{Binding ShowPreviewCommand}"
Content="UI Preview"
Padding="4"
Margin="0,8,0,0">
<!--<Button.Style>
<Style BasedOn="{StaticResource {x:Type Button}}" TargetType="{x:Type Button}">
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</Setter.Value>
</Setter>
</Style>
</Button.Style>-->
</Button>
<ui:HyperlinkButton Padding="4"
NavigateUri="{Binding SelectedPackage.LicenseUrl}">
<TextBlock TextWrapping="Wrap">
<Run Text="{Binding SelectedPackage.LicenseType, Mode=OneWay}" />
<Run Text="License" />
</TextBlock>
</ui:HyperlinkButton>
<ui:HyperlinkButton Padding="4"
NavigateUri="{Binding SelectedPackage.GithubUrl}">
<TextBlock TextWrapping="Wrap">
<Run Text="GitHub Page:" />
<Run Text="{Binding SelectedPackage.GithubUrl, Mode=OneWay}" TextDecorations="Underline" />
</TextBlock> </TextBlock>
</ui:HyperlinkButton> </StackPanel>
<TextBlock Text="{Binding SelectedPackage.Disclaimer}" <!-- Version Selector -->
Padding="4" <Label Grid.Row="1" Grid.Column="1"
Foreground="OrangeRed" Content="{Binding ReleaseLabelText}" />
TextWrapping="Wrap" <ui:FAComboBox Grid.Row="2" Grid.Column="1"
IsVisible="{Binding SelectedPackage.Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> ItemsSource="{Binding AvailableVersions}"
MinWidth="250"
VerticalAlignment="Stretch"
SelectedItem="{Binding SelectedVersion}">
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:PackageVersion}">
<TextBlock
Name="NameTextBlock"
VerticalAlignment="Center"
Text="{Binding TagName}" />
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0" <StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4"
HorizontalAlignment="Center"
Orientation="Horizontal" Margin="0,8,0,8"
IsVisible="{Binding IsReleaseModeAvailable}"> IsVisible="{Binding IsReleaseModeAvailable}">
<ToggleButton <ToggleButton
Content="Releases" Content="Releases"
@ -147,103 +107,122 @@
Margin="8,0,0,0" /> Margin="8,0,0,0" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,16,0,0" Orientation="Horizontal"> <!-- Advanced Options Button -->
<StackPanel Orientation="Vertical"> <Button Grid.Row="2" Grid.Column="2" VerticalAlignment="Stretch"
<Label Content="{Binding ReleaseLabelText}" /> Margin="8,0">
<ComboBox <ui:SymbolIcon FontSize="16" Margin="4" Symbol="Settings" />
ItemsSource="{Binding AvailableVersions}" <Button.Flyout>
MinWidth="200" <Flyout>
SelectedItem="{Binding SelectedVersion}"> <!-- Advanced Options -->
<ComboBox.ItemTemplate> <StackPanel Orientation="Vertical"
<DataTemplate DataType="{x:Type models:PackageVersion}"> Margin="8"
<StackPanel Margin="8,0,0,0" VerticalAlignment="Top"> IsVisible="{Binding !IsAdvancedMode}">
<TextBlock
Margin="0,4,0,4" <Label Content="Advanced Options"
Name="NameTextBlock" FontSize="18"
Text="{Binding TagName}" /> HorizontalAlignment="Center"
</StackPanel> Margin="8,0,8,8" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<StackPanel <Label Content="Commit"
Margin="8,0,0,0" Margin="0,0,0,4"
Orientation="Vertical" IsVisible="{Binding !IsReleaseMode}" />
IsVisible="{Binding !IsReleaseMode}"> <ui:FAComboBox
<Label Content="Commit" /> IsVisible="{Binding !IsReleaseMode}"
<ComboBox
ItemsSource="{Binding AvailableCommits}" ItemsSource="{Binding AvailableCommits}"
MinWidth="100" MinWidth="200"
MinHeight="38"
SelectedItem="{Binding SelectedCommit}"> SelectedItem="{Binding SelectedCommit}">
<ComboBox.ItemTemplate> <ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type database:GitCommit}"> <DataTemplate DataType="{x:Type database:GitCommit}">
<StackPanel Margin="8,0,0,0" VerticalAlignment="Top">
<TextBlock <TextBlock
Margin="0,4,0,4" Margin="8,4,0,4"
Name="NameTextBlock" Name="NameTextBlock"
Text="{Binding Sha}" /> Text="{Binding ShortSha}" />
</StackPanel>
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ui:FAComboBox.ItemTemplate>
</ComboBox> </ui:FAComboBox>
</StackPanel>
</StackPanel>
<Label Content="Display Name" Margin="0,16,0,0" /> <Label Content="Shared Model Folder Strategy"
<StackPanel Orientation="Horizontal" IsVisible="{Binding ShowDuplicateWarning}"> Margin="0,8,0,4" />
<ui:SymbolIcon <ui:FAComboBox
Foreground="{DynamicResource ThemeRedColor}" ItemsSource="{Binding SelectedPackage.AvailableSharedFolderMethods}"
Margin="8" MinWidth="200"
Symbol="Alert" /> MinHeight="38"
SelectedItem="{Binding SelectedSharedFolderMethod}">
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:SharedFolderMethod}">
<TextBlock <TextBlock
Foreground="{DynamicResource ThemeRedColor}" Margin="8,4,0,4"
Margin="0,8,8,8" Text="{Binding }" />
TextAlignment="Left" </DataTemplate>
TextWrapping="Wrap"> </ui:FAComboBox.ItemTemplate>
<Run Text="An installation with this name already exists." /> </ui:FAComboBox>
<LineBreak />
<Run Text="Please choose a different name or select a different Install Location." /> <Label Content="PyTorch Version"
</TextBlock> IsVisible="{Binding ShowTorchVersionOptions}"
Margin="0,8,0,4" />
<ui:FAComboBox
ItemsSource="{Binding SelectedPackage.AvailableTorchVersions}"
MinWidth="200"
MinHeight="38"
IsVisible="{Binding ShowTorchVersionOptions}"
SelectedItem="{Binding SelectedTorchVersion}">
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:TorchVersion}">
<TextBlock
Margin="8,4,0,4"
Text="{Binding }" />
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
</StackPanel> </StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<TextBox <!-- Install Button -->
Margin="0,0,0,8" <UniformGrid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
Text="{Binding InstallName, Mode=TwoWay}" /> Columns="2"
Margin="0,32,0,0"
<StackPanel Orientation="Horizontal"> HorizontalAlignment="Center">
<Button
Content="Cancel"
Command="{Binding Cancel}"
FontSize="20"
HorizontalAlignment="Center"
Margin="8,0,4,0"
Padding="16, 8, 16, 8" />
<Button <Button
Command="{Binding InstallCommand}"
Content="Install" Content="Install"
Command="{Binding InstallCommand}"
FontSize="20"
IsEnabled="{Binding CanInstall}"
HorizontalAlignment="Center"
Classes="success" Classes="success"
Height="50" Margin="4,0,8,0"
IsEnabled="{Binding !ShowDuplicateWarning}" Padding="16, 8, 16, 8" />
Margin="0,16,0,0" </UniformGrid>
VerticalAlignment="Top"
Width="100" /> <!-- Available Packages -->
<controls:ProgressRing <ListBox Grid.Column="1" Grid.Row="0"
Height="25" Margin="8, 16"
IsIndeterminate="True" ItemsSource="{Binding AvailablePackages}"
BorderThickness="4" SelectedItem="{Binding SelectedPackage}">
Margin="8,16,0,0" <ListBox.Template>
VerticalAlignment="Center" <ControlTemplate>
IsVisible="{Binding InstallProgress.IsProgressVisible}" <ItemsPresenter />
Width="25" /> </ControlTemplate>
<TextBlock </ListBox.Template>
Margin="8,16,0,0" <ListBox.ItemTemplate>
Text="Installing..." <DataTemplate DataType="{x:Type packages:BasePackage}">
VerticalAlignment="Center" <StackPanel Margin="8">
IsVisible="{Binding InstallProgress.IsProgressVisible}" /> <TextBlock Text="{Binding DisplayName}" />
</StackPanel> <TextBlock Text="{Binding ByAuthor}" />
</StackPanel> </StackPanel>
<ScrollViewer Grid.Column="2" </DataTemplate>
MaxWidth="300" </ListBox.ItemTemplate>
Margin="16" </ListBox>
MaxHeight="600">
<mdxaml:MarkdownScrollViewer
Markdown="{Binding ReleaseNotes, Mode=OneWay}"/>
</ScrollViewer>
</Grid>
</Grid> </Grid>
</controls:UserControlBase> </controls:UserControlBase>

56
StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml

@ -0,0 +1,56 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:base="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="base:ContentDialogProgressViewModelBase"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PackageModificationDialog">
<Grid Margin="8" RowDefinitions="Auto, Auto, Auto, *, Auto, Auto">
<TextBlock Grid.Row="0" Text="{Binding Text}"
FontSize="16"
Margin="4"
TextWrapping="WrapWithOverflow"
TextAlignment="Center"
HorizontalAlignment="Stretch"/>
<TextBlock Grid.Row="1" Text="{Binding Description}"
Margin="4"
TextWrapping="WrapWithOverflow"
TextAlignment="Center"
IsVisible="{Binding !#Expander.IsExpanded}"/>
<ProgressBar Grid.Row="2" Value="{Binding Value}"
Margin="8"
IsIndeterminate="{Binding IsIndeterminate}"/>
<Expander Grid.Row="3"
Margin="8"
Header="More Details" x:Name="Expander">
<avaloniaEdit:TextEditor
x:Name="Console"
Margin="8"
MaxHeight="400"
DataContext="{Binding Console}"
Document="{Binding Document}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
IsReadOnly="True"
LineNumbersForeground="DarkSlateGray"
ShowLineNumbers="True"
VerticalScrollBarVisibility="Auto"
WordWrap="True" />
</Expander>
<CheckBox Grid.Row="4" IsChecked="{Binding CloseWhenFinished}"
HorizontalAlignment="Center"
Margin="4" Content="Close dialog when finished"/>
<Button Grid.Row="5" Content="Close"
FontSize="20"
HorizontalAlignment="Center"
Command="{Binding OnCloseButtonClick}"/>
</Grid>
</controls:UserControlBase>

51
StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs

@ -0,0 +1,51 @@
using System;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using AvaloniaEdit;
using AvaloniaEdit.TextMate;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Helper;
using TextMateSharp.Grammars;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
public partial class PackageModificationDialog : UserControlBase
{
public PackageModificationDialog()
{
InitializeComponent();
var editor = this.FindControl<TextEditor>("Console");
if (editor is not null)
{
var options = new RegistryOptions(ThemeName.DarkPlus);
// Config hyperlinks
editor.TextArea.Options.EnableHyperlinks = true;
editor.TextArea.Options.RequireControlModifierForHyperlinkClick = false;
editor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Coral;
var textMate = editor.InstallTextMate(options);
var scope = options.GetScopeByLanguageId("log");
if (scope is null)
throw new InvalidOperationException("Scope is null");
textMate.SetGrammar(scope);
textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus));
}
EventManager.Instance.ScrollToBottomRequested += (_, _) =>
{
Dispatcher.UIThread.Invoke(() =>
{
var editor = this.FindControl<TextEditor>("Console");
if (editor?.Document == null)
return;
var line = Math.Max(editor.Document.LineCount - 1, 1);
editor.ScrollToLine(line);
});
};
}
}

6
StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs

@ -31,7 +31,8 @@ public partial class LaunchPageView : UserControlBase
var textMate = editor.InstallTextMate(options); var textMate = editor.InstallTextMate(options);
var scope = options.GetScopeByLanguageId("log"); var scope = options.GetScopeByLanguageId("log");
if (scope is null) throw new InvalidOperationException("Scope is null"); if (scope is null)
throw new InvalidOperationException("Scope is null");
textMate.SetGrammar(scope); textMate.SetGrammar(scope);
textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus)); textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus));
@ -55,7 +56,8 @@ public partial class LaunchPageView : UserControlBase
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
var editor = this.FindControl<TextEditor>("Console"); var editor = this.FindControl<TextEditor>("Console");
if (editor?.Document == null) return; if (editor?.Document == null)
return;
var line = Math.Max(editor.Document.LineCount - LineOffset, 1); var line = Math.Max(editor.Document.LineCount - LineOffset, 1);
editor.ScrollToLine(line); editor.ScrollToLine(line);
}); });

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

@ -25,6 +25,7 @@ using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
#if DEBUG #if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.Views; using StabilityMatrix.Avalonia.Diagnostics.Views;
@ -38,6 +39,8 @@ public partial class MainWindow : AppWindowBase
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly INavigationService navigationService; private readonly INavigationService navigationService;
private FlyoutBase? progressFlyout;
[DesignOnly(true)] [DesignOnly(true)]
[SuppressMessage("ReSharper", "UnusedMember.Global")] [SuppressMessage("ReSharper", "UnusedMember.Global")]
public MainWindow() public MainWindow()
@ -62,6 +65,8 @@ public partial class MainWindow : AppWindowBase
#endif #endif
TitleBar.ExtendsContentIntoTitleBar = true; TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
EventManager.Instance.ToggleProgressFlyout += (_, _) => progressFlyout?.Hide();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -231,6 +236,8 @@ public partial class MainWindow : AppWindowBase
var item = sender as NavigationViewItem; var item = sender as NavigationViewItem;
var flyout = item!.ContextFlyout; var flyout = item!.ContextFlyout;
flyout!.ShowAt(item); flyout!.ShowAt(item);
progressFlyout = flyout;
} }
private void FooterUpdateItem_OnTapped(object? sender, TappedEventArgs e) private void FooterUpdateItem_OnTapped(object? sender, TappedEventArgs e)

69
StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml

@ -11,11 +11,12 @@
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmBase="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base" xmlns:vmBase="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base"
xmlns:progress="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Progress"
MaxHeight="250" MaxHeight="250"
d:DataContext="{x:Static mocks:DesignData.ProgressManagerViewModel}" d:DataContext="{x:Static mocks:DesignData.ProgressManagerViewModel}"
d:DesignHeight="250" d:DesignHeight="250"
d:DesignWidth="300" d:DesignWidth="300"
x:DataType="vm:ProgressManagerViewModel" x:DataType="progress:ProgressManagerViewModel"
mc:Ignorable="d"> mc:Ignorable="d">
<ScrollViewer> <ScrollViewer>
<Grid RowDefinitions="Auto, *"> <Grid RowDefinitions="Auto, *">
@ -117,7 +118,7 @@
</Border> </Border>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type vm:ProgressItemViewModel}"> <DataTemplate DataType="{x:Type progress:ProgressItemViewModel}">
<Border <Border
Margin="4" Margin="4"
Padding="8" Padding="8"
@ -164,6 +165,70 @@
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type progress:PackageInstallProgressItemViewModel}">
<Border
Margin="4"
Padding="8"
Background="#22000000"
BorderBrush="#33000000"
BorderThickness="2"
CornerRadius="8">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<StackPanel Grid.Row="0" Grid.Column="0">
<TextBlock Margin="0,0"
Text="{Binding Progress.Text, Mode=OneWay}" />
<!-- non-indeterminate progress -->
<TextBlock
Margin="0,4"
MaxWidth="300"
IsVisible="{Binding !Progress.IsIndeterminate}">
<Run Text="{Binding Progress.Description, Mode=OneWay}" />
<Run Text="{Binding Progress.Value, Mode=OneWay}" /><Run Text="%" />
</TextBlock>
<!-- indeterminate progress -->
<TextBlock
Margin="0,4"
MaxWidth="300"
IsVisible="{Binding Progress.IsIndeterminate}"
Text="{Binding Progress.Description, Mode=OneWay}" />
</StackPanel>
<!-- Buttons -->
<Button
Grid.Row="0"
Grid.Column="1"
Margin="8,0"
ToolTip.Tip="Show Progress Dialog"
Command="{Binding ShowProgressDialog}"
Classes="transparent-full">
<ui:SymbolIcon Symbol="Open" />
</Button>
<ProgressBar
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0,8,0,4"
IsIndeterminate="{Binding Progress.IsIndeterminate}"
Value="{Binding Progress.Value, Mode=OneWay}">
<ProgressBar.Transitions>
<Transitions>
<DoubleTransition Property="Value" Duration="00:00:00.150">
<DoubleTransition.Easing>
<SineEaseInOut />
</DoubleTransition.Easing>
</DoubleTransition>
</Transitions>
</ProgressBar.Transitions>
</ProgressBar>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.DataTemplates> </ItemsControl.DataTemplates>
</ItemsControl> </ItemsControl>
</Grid> </Grid>

1
StabilityMatrix.Core/Database/ILiteDbContext.cs

@ -13,7 +13,6 @@ public interface ILiteDbContext : IDisposable
ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache { get; } ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache { get; }
ILiteCollectionAsync<LocalModelFile> LocalModelFiles { get; } ILiteCollectionAsync<LocalModelFile> LocalModelFiles { get; }
Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3); Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3);
Task<bool> UpsertCivitModelAsync(CivitModel civitModel); Task<bool> UpsertCivitModelAsync(CivitModel civitModel);
Task<bool> UpsertCivitModelAsync(IEnumerable<CivitModel> civitModels); Task<bool> UpsertCivitModelAsync(IEnumerable<CivitModel> civitModels);

77
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -23,16 +23,22 @@ public class LiteDbContext : ILiteDbContext
public event EventHandler? CivitModelsChanged; public event EventHandler? CivitModelsChanged;
// Collections (Tables) // Collections (Tables)
public ILiteCollectionAsync<CivitModel> CivitModels => Database.GetCollection<CivitModel>("CivitModels"); public ILiteCollectionAsync<CivitModel> CivitModels =>
public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions => Database.GetCollection<CivitModelVersion>("CivitModelVersions"); Database.GetCollection<CivitModel>("CivitModels");
public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache => Database.GetCollection<CivitModelQueryCacheEntry>("CivitModelQueryCache"); public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions =>
public ILiteCollectionAsync<GithubCacheEntry> GithubCache => Database.GetCollection<GithubCacheEntry>("GithubCache"); Database.GetCollection<CivitModelVersion>("CivitModelVersions");
public ILiteCollectionAsync<LocalModelFile> LocalModelFiles => Database.GetCollection<LocalModelFile>("LocalModelFiles"); public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache =>
Database.GetCollection<CivitModelQueryCacheEntry>("CivitModelQueryCache");
public ILiteCollectionAsync<GithubCacheEntry> GithubCache =>
Database.GetCollection<GithubCacheEntry>("GithubCache");
public ILiteCollectionAsync<LocalModelFile> LocalModelFiles =>
Database.GetCollection<LocalModelFile>("LocalModelFiles");
public LiteDbContext( public LiteDbContext(
ILogger<LiteDbContext> logger, ILogger<LiteDbContext> logger,
ISettingsManager settingsManager, ISettingsManager settingsManager,
IOptions<DebugOptions> debugOptions) IOptions<DebugOptions> debugOptions
)
{ {
this.logger = logger; this.logger = logger;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
@ -53,15 +59,20 @@ public class LiteDbContext : ILiteDbContext
try try
{ {
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db"); var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync(new ConnectionString() db = new LiteDatabaseAsync(
new ConnectionString()
{ {
Filename = dbPath, Filename = dbPath,
Connection = ConnectionType.Shared, Connection = ConnectionType.Shared,
}); }
);
} }
catch (IOException e) catch (IOException e)
{ {
logger.LogWarning("Database in use or not accessible ({Message}), using temporary database", e.Message); logger.LogWarning(
"Database in use or not accessible ({Message}), using temporary database",
e.Message
);
} }
} }
@ -69,30 +80,43 @@ public class LiteDbContext : ILiteDbContext
db ??= new LiteDatabaseAsync(":temp:"); db ??= new LiteDatabaseAsync(":temp:");
// Register reference fields // Register reference fields
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(m => m.ModelVersions, "CivitModelVersions"); LiteDBExtensions.Register<CivitModel, CivitModelVersion>(
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(e => e.Items, "CivitModels"); m => m.ModelVersions,
"CivitModelVersions"
);
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(
e => e.Items,
"CivitModels"
);
return db; return db;
} }
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3) public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(
string hashBlake3
)
{ {
var version = await CivitModelVersions.Query() var version = await CivitModelVersions
.Where(mv => mv.Files! .Query()
.Where(
mv =>
mv.Files!
.Select(f => f.Hashes) .Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3) .Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3)) .Any(hash => hash == hashBlake3)
)
.FirstOrDefaultAsync() .FirstOrDefaultAsync()
.ConfigureAwait(false); .ConfigureAwait(false);
if (version is null) return (null, null); if (version is null)
return (null, null);
var model = await CivitModels.Query() var model = await CivitModels
.Query()
.Include(m => m.ModelVersions) .Include(m => m.ModelVersions)
.Where(m => m.ModelVersions! .Where(m => m.ModelVersions!.Select(v => v.Id).Any(id => id == version.Id))
.Select(v => v.Id) .FirstOrDefaultAsync()
.Any(id => id == version.Id)) .ConfigureAwait(false);
.FirstOrDefaultAsync().ConfigureAwait(false);
return (model, version); return (model, version);
} }
@ -100,7 +124,9 @@ public class LiteDbContext : ILiteDbContext
public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel) public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel)
{ {
// Insert model versions first then model // Insert model versions first then model
var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions).ConfigureAwait(false); var versionsUpdated = await CivitModelVersions
.UpsertAsync(civitModel.ModelVersions)
.ConfigureAwait(false);
var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false); var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false);
// Notify listeners on any change // Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated; var anyUpdated = versionsUpdated > 0 || updated;
@ -141,7 +167,8 @@ public class LiteDbContext : ILiteDbContext
public async Task<GithubCacheEntry?> GetGithubCacheEntry(string? cacheKey) public async Task<GithubCacheEntry?> GetGithubCacheEntry(string? cacheKey)
{ {
if (string.IsNullOrEmpty(cacheKey)) return null; if (string.IsNullOrEmpty(cacheKey))
return null;
if (await GithubCache.FindByIdAsync(cacheKey).ConfigureAwait(false) is { } result) if (await GithubCache.FindByIdAsync(cacheKey).ConfigureAwait(false) is { } result)
{ {
@ -162,9 +189,7 @@ public class LiteDbContext : ILiteDbContext
{ {
database.Dispose(); database.Dispose();
} }
catch (ObjectDisposedException) catch (ObjectDisposedException) { }
{
}
database = null; database = null;
} }

40
StabilityMatrix.Core/Helper/EventManager.cs

@ -1,4 +1,5 @@
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Models.Update;
@ -10,10 +11,7 @@ public class EventManager
{ {
public static EventManager Instance { get; } = new(); public static EventManager Instance { get; } = new();
private EventManager() private EventManager() { }
{
}
public event EventHandler<int>? GlobalProgressChanged; public event EventHandler<int>? GlobalProgressChanged;
public event EventHandler? InstalledPackagesChanged; public event EventHandler? InstalledPackagesChanged;
@ -26,18 +24,40 @@ public class EventManager
public event EventHandler<ProgressItem>? ProgressChanged; public event EventHandler<ProgressItem>? ProgressChanged;
public event EventHandler<RunningPackageStatusChangedEventArgs>? RunningPackageStatusChanged; public event EventHandler<RunningPackageStatusChangedEventArgs>? RunningPackageStatusChanged;
public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress); public event EventHandler<IPackageModificationRunner>? PackageInstallProgressAdded;
public void OnInstalledPackagesChanged() => InstalledPackagesChanged?.Invoke(this, EventArgs.Empty); public event EventHandler? ToggleProgressFlyout;
public void OnOneClickInstallFinished(bool skipped) => OneClickInstallFinished?.Invoke(this, skipped);
public void OnGlobalProgressChanged(int progress) =>
GlobalProgressChanged?.Invoke(this, progress);
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); public void OnTeachingTooltipNeeded() => TeachingTooltipNeeded?.Invoke(this, EventArgs.Empty);
public void OnDevModeSettingChanged(bool value) => DevModeSettingChanged?.Invoke(this, value); public void OnDevModeSettingChanged(bool value) => DevModeSettingChanged?.Invoke(this, value);
public void OnUpdateAvailable(UpdateInfo args) => UpdateAvailable?.Invoke(this, args); public void OnUpdateAvailable(UpdateInfo args) => UpdateAvailable?.Invoke(this, args);
public void OnPackageLaunchRequested(Guid packageId) => public void OnPackageLaunchRequested(Guid packageId) =>
PackageLaunchRequested?.Invoke(this, packageId); PackageLaunchRequested?.Invoke(this, packageId);
public void OnScrollToBottomRequested() => public void OnScrollToBottomRequested() =>
ScrollToBottomRequested?.Invoke(this, EventArgs.Empty); ScrollToBottomRequested?.Invoke(this, EventArgs.Empty);
public void OnProgressChanged(ProgressItem progress) =>
ProgressChanged?.Invoke(this, progress); public void OnProgressChanged(ProgressItem progress) => ProgressChanged?.Invoke(this, progress);
public void OnRunningPackageStatusChanged(PackagePair? currentPackagePair) => public void OnRunningPackageStatusChanged(PackagePair? currentPackagePair) =>
RunningPackageStatusChanged?.Invoke(this, new RunningPackageStatusChangedEventArgs(currentPackagePair)); RunningPackageStatusChanged?.Invoke(
this,
new RunningPackageStatusChangedEventArgs(currentPackagePair)
);
public void OnPackageInstallProgressAdded(IPackageModificationRunner runner) =>
PackageInstallProgressAdded?.Invoke(this, runner);
public void OnToggleProgressFlyout() => ToggleProgressFlyout?.Invoke(this, EventArgs.Empty);
} }

65
StabilityMatrix.Core/Helper/SharedFolders.cs

@ -36,8 +36,11 @@ public class SharedFolders : ISharedFolders
} }
} }
public static void SetupLinks(Dictionary<SharedFolderType, IReadOnlyList<string>> definitions, public static void SetupLinks(
DirectoryPath modelsDirectory, DirectoryPath installDirectory) Dictionary<SharedFolderType, IReadOnlyList<string>> definitions,
DirectoryPath modelsDirectory,
DirectoryPath installDirectory
)
{ {
foreach (var (folderType, relativePaths) in definitions) foreach (var (folderType, relativePaths) in definitions)
{ {
@ -63,7 +66,9 @@ public class SharedFolders : ISharedFolders
// Skip name collisions // Skip name collisions
if (File.Exists(sourceFile)) if (File.Exists(sourceFile))
{ {
Logger.Warn($"Skipping file {file.FullName} because it already exists in {sourceDir}"); Logger.Warn(
$"Skipping file {file.FullName} because it already exists in {sourceDir}"
);
continue; continue;
} }
destinationFile.Info.MoveTo(sourceFile); destinationFile.Info.MoveTo(sourceFile);
@ -81,7 +86,8 @@ public class SharedFolders : ISharedFolders
{ {
var modelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var modelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var sharedFolders = basePackage.SharedFolders; var sharedFolders = basePackage.SharedFolders;
if (sharedFolders == null) return; if (sharedFolders == null)
return;
SetupLinks(sharedFolders, modelsDirectory, installDirectory); SetupLinks(sharedFolders, modelsDirectory, installDirectory);
} }
@ -89,10 +95,15 @@ public class SharedFolders : ISharedFolders
/// Deletes junction links and remakes them. Unlike SetupLinksForPackage, /// Deletes junction links and remakes them. Unlike SetupLinksForPackage,
/// this will not copy files from the destination to the source. /// this will not copy files from the destination to the source.
/// </summary> /// </summary>
public static async Task UpdateLinksForPackage(BasePackage basePackage, DirectoryPath modelsDirectory, DirectoryPath installDirectory) public static async Task UpdateLinksForPackage(
BasePackage basePackage,
DirectoryPath modelsDirectory,
DirectoryPath installDirectory
)
{ {
var sharedFolders = basePackage.SharedFolders; var sharedFolders = basePackage.SharedFolders;
if (sharedFolders is null) return; if (sharedFolders is null)
return;
foreach (var (folderType, relativePaths) in sharedFolders) foreach (var (folderType, relativePaths) in sharedFolders)
{ {
@ -117,7 +128,8 @@ public class SharedFolders : ISharedFolders
if (destinationDir.Info.LinkTarget == sourceDir) if (destinationDir.Info.LinkTarget == sourceDir)
{ {
Logger.Info( Logger.Info(
$"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})"); $"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})"
);
return; return;
} }
@ -131,8 +143,12 @@ public class SharedFolders : ISharedFolders
if (destinationDir.Info.EnumerateFileSystemInfos().Any()) if (destinationDir.Info.EnumerateFileSystemInfos().Any())
{ {
Logger.Info($"Moving files from {destinationDir} to {sourceDir}"); Logger.Info($"Moving files from {destinationDir} to {sourceDir}");
await FileTransfers.MoveAllFilesAndDirectories( await FileTransfers
destinationDir, sourceDir, overwriteIfHashMatches: true) .MoveAllFilesAndDirectories(
destinationDir,
sourceDir,
overwriteIfHashMatches: true
)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@ -161,7 +177,8 @@ public class SharedFolders : ISharedFolders
{ {
var destination = Path.GetFullPath(Path.Combine(installPath, relativePath)); var destination = Path.GetFullPath(Path.Combine(installPath, relativePath));
// Delete the destination folder if it exists // Delete the destination folder if it exists
if (!Directory.Exists(destination)) continue; if (!Directory.Exists(destination))
continue;
Logger.Info($"Deleting junction target {destination}"); Logger.Info($"Deleting junction target {destination}");
Directory.Delete(destination, false); Directory.Delete(destination, false);
@ -174,19 +191,32 @@ public class SharedFolders : ISharedFolders
var packages = settingsManager.Settings.InstalledPackages; var packages = settingsManager.Settings.InstalledPackages;
foreach (var package in packages) foreach (var package in packages)
{ {
if (package.PackageName == null) continue; if (package.PackageName == null)
continue;
var basePackage = packageFactory[package.PackageName]; var basePackage = packageFactory[package.PackageName];
if (basePackage == null) continue; if (basePackage == null)
if (package.LibraryPath == null) continue; continue;
if (package.LibraryPath == null)
continue;
try try
{ {
basePackage.RemoveModelFolderLinks(package.FullPath).GetAwaiter().GetResult(); var sharedFolderMethod =
package.PreferredSharedFolderMethod
?? basePackage.RecommendedSharedFolderMethod;
basePackage
.RemoveModelFolderLinks(package.FullPath, sharedFolderMethod)
.GetAwaiter()
.GetResult();
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn("Failed to remove links for package {Package} " + Logger.Warn(
"({DisplayName}): {Message}", package.PackageName, package.DisplayName, e.Message); "Failed to remove links for package {Package} " + "({DisplayName}): {Message}",
package.PackageName,
package.DisplayName,
e.Message
);
} }
} }
} }
@ -194,7 +224,8 @@ public class SharedFolders : ISharedFolders
public void SetupSharedModelFolders() public void SetupSharedModelFolders()
{ {
var modelsDir = settingsManager.ModelsDirectory; var modelsDir = settingsManager.ModelsDirectory;
if (string.IsNullOrWhiteSpace(modelsDir)) return; if (string.IsNullOrWhiteSpace(modelsDir))
return;
Directory.CreateDirectory(modelsDir); Directory.CreateDirectory(modelsDir);
var allSharedFolderTypes = Enum.GetValues<SharedFolderType>(); var allSharedFolderTypes = Enum.GetValues<SharedFolderType>();

7
StabilityMatrix.Core/Models/Database/GitCommit.cs

@ -1,6 +1,11 @@
namespace StabilityMatrix.Core.Models.Database; using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Database;
public class GitCommit public class GitCommit
{ {
public string? Sha { get; set; } public string? Sha { get; set; }
[JsonIgnore]
public string ShortSha => string.IsNullOrWhiteSpace(Sha) ? string.Empty : Sha[..7];
} }

3
StabilityMatrix.Core/Models/Database/LocalModelFile.cs

@ -35,7 +35,8 @@ public class LocalModelFile
public string? GetPreviewImageFullPath(string rootModelDirectory) public string? GetPreviewImageFullPath(string rootModelDirectory)
{ {
return PreviewImageRelativePath == null ? null return PreviewImageRelativePath == null
? null
: Path.Combine(rootModelDirectory, PreviewImageRelativePath); : Path.Combine(rootModelDirectory, PreviewImageRelativePath);
} }

8
StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs

@ -0,0 +1,8 @@
namespace StabilityMatrix.Core.Models;
public class DownloadPackageVersionOptions
{
public string BranchName { get; set; }
public string CommitHash { get; set; }
public string VersionTag { get; set; }
}

29
StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs

@ -10,6 +10,7 @@ namespace StabilityMatrix.Core.Models.FileInterfaces;
public class FilePath : FileSystemPath, IPathObject public class FilePath : FileSystemPath, IPathObject
{ {
private FileInfo? _info; private FileInfo? _info;
// ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global
[JsonIgnore] [JsonIgnore]
public FileInfo Info => _info ??= new FileInfo(FullPath); public FileInfo Info => _info ??= new FileInfo(FullPath);
@ -31,8 +32,7 @@ public class FilePath : FileSystemPath, IPathObject
public string Name => Info.Name; public string Name => Info.Name;
[JsonIgnore] [JsonIgnore]
public string NameWithoutExtension public string NameWithoutExtension => Path.GetFileNameWithoutExtension(Info.Name);
=> Path.GetFileNameWithoutExtension(Info.Name);
/// <summary> /// <summary>
/// Get the directory of the file. /// Get the directory of the file.
@ -44,8 +44,7 @@ public class FilePath : FileSystemPath, IPathObject
{ {
try try
{ {
return Info.Directory == null ? null return Info.Directory == null ? null : new DirectoryPath(Info.Directory);
: new DirectoryPath(Info.Directory);
} }
catch (DirectoryNotFoundException) catch (DirectoryNotFoundException)
{ {
@ -54,22 +53,20 @@ public class FilePath : FileSystemPath, IPathObject
} }
} }
public FilePath(string path) : base(path) public FilePath(string path)
{ : base(path) { }
}
public FilePath(FileInfo fileInfo) : base(fileInfo.FullName) public FilePath(FileInfo fileInfo)
: base(fileInfo.FullName)
{ {
_info = fileInfo; _info = fileInfo;
} }
public FilePath(FileSystemPath path) : base(path) public FilePath(FileSystemPath path)
{ : base(path) { }
}
public FilePath(params string[] paths) : base(paths) public FilePath(params string[] paths)
{ : base(paths) { }
}
public long GetSize() public long GetSize()
{ {
@ -79,7 +76,8 @@ public class FilePath : FileSystemPath, IPathObject
public long GetSize(bool includeSymbolicLinks) public long GetSize(bool includeSymbolicLinks)
{ {
if (!includeSymbolicLinks && IsSymbolicLink) return 0; if (!includeSymbolicLinks && IsSymbolicLink)
return 0;
return GetSize(); return GetSize();
} }
@ -174,5 +172,6 @@ public class FilePath : FileSystemPath, IPathObject
// Implicit conversions to and from string // Implicit conversions to and from string
public static implicit operator string(FilePath path) => path.FullPath; public static implicit operator string(FilePath path) => path.FullPath;
public static implicit operator FilePath(string path) => new(path); public static implicit operator FilePath(string path) => new(path);
} }

72
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -6,19 +6,29 @@ namespace StabilityMatrix.Core.Models;
/// <summary> /// <summary>
/// Profile information for a user-installed package. /// Profile information for a user-installed package.
/// </summary> /// </summary>
public class InstalledPackage public class InstalledPackage : IJsonOnDeserialized
{ {
// Unique ID for the installation // Unique ID for the installation
public Guid Id { get; set; } public Guid Id { get; set; }
// User defined name // User defined name
public string? DisplayName { get; set; } public string? DisplayName { get; set; }
// Package name // Package name
public string? PackageName { get; set; } public string? PackageName { get; set; }
// Package version // Package version
[Obsolete("Use Version instead. (Kept for migration)")]
public string? PackageVersion { get; set; } public string? PackageVersion { get; set; }
[Obsolete("Use Version instead. (Kept for migration)")]
public string? InstalledBranch { get; set; } public string? InstalledBranch { get; set; }
[Obsolete("Use Version instead. (Kept for migration)")]
public string? DisplayVersion { get; set; } public string? DisplayVersion { get; set; }
public InstalledPackageVersion? Version { get; set; }
// Old type absolute path // Old type absolute path
[Obsolete("Use LibraryPath instead. (Kept for migration)")] [Obsolete("Use LibraryPath instead. (Kept for migration)")]
public string? Path { get; set; } public string? Path { get; set; }
@ -32,13 +42,15 @@ public class InstalledPackage
/// Full path to the package, using LibraryPath and GlobalConfig.LibraryDir. /// Full path to the package, using LibraryPath and GlobalConfig.LibraryDir.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public string? FullPath => LibraryPath != null ? System.IO.Path.Combine(GlobalConfig.LibraryDir, LibraryPath) : null; public string? FullPath =>
LibraryPath != null ? System.IO.Path.Combine(GlobalConfig.LibraryDir, LibraryPath) : null;
public string? LaunchCommand { get; set; } public string? LaunchCommand { get; set; }
public List<LaunchOption>? LaunchArgs { get; set; } public List<LaunchOption>? LaunchArgs { get; set; }
public DateTimeOffset? LastUpdateCheck { get; set; } public DateTimeOffset? LastUpdateCheck { get; set; }
public bool UpdateAvailable { get; set; } public bool UpdateAvailable { get; set; }
public TorchVersion? PreferredTorchVersion { get; set; }
public SharedFolderMethod? PreferredSharedFolderMethod { get; set; }
/// <summary> /// <summary>
/// Get the path as a relative sub-path of the relative path. /// Get the path as a relative sub-path of the relative path.
@ -48,9 +60,11 @@ public class InstalledPackage
{ {
var relativePath = System.IO.Path.GetRelativePath(relativeTo, path); var relativePath = System.IO.Path.GetRelativePath(relativeTo, path);
// GetRelativePath returns the path if it's not relative // GetRelativePath returns the path if it's not relative
if (relativePath == path) return null; if (relativePath == path)
return null;
// Further check if the path is a sub-path of the library // Further check if the path is a sub-path of the library
var isSubPath = relativePath != "." var isSubPath =
relativePath != "."
&& relativePath != ".." && relativePath != ".."
&& !relativePath.StartsWith(".." + System.IO.Path.DirectorySeparatorChar) && !relativePath.StartsWith(".." + System.IO.Path.DirectorySeparatorChar)
&& !System.IO.Path.IsPathRooted(relativePath); && !System.IO.Path.IsPathRooted(relativePath);
@ -67,7 +81,8 @@ public class InstalledPackage
#pragma warning disable CS0618 #pragma warning disable CS0618
var oldPath = Path; var oldPath = Path;
#pragma warning restore CS0618 #pragma warning restore CS0618
if (oldPath == null) return false; if (oldPath == null)
return false;
// Check if the path is a sub-path of the library // Check if the path is a sub-path of the library
var library = libraryDirectory ?? GlobalConfig.LibraryDir; var library = libraryDirectory ?? GlobalConfig.LibraryDir;
@ -96,7 +111,8 @@ public class InstalledPackage
#pragma warning disable CS0618 #pragma warning disable CS0618
var oldPath = Path; var oldPath = Path;
#pragma warning restore CS0618 #pragma warning restore CS0618
if (oldPath == null) return false; if (oldPath == null)
return false;
// Check if the path is a sub-path of the library // Check if the path is a sub-path of the library
var library = libraryDirectory ?? GlobalConfig.LibraryDir; var library = libraryDirectory ?? GlobalConfig.LibraryDir;
@ -114,7 +130,8 @@ public class InstalledPackage
#pragma warning disable CS0618 #pragma warning disable CS0618
var oldPath = Path; var oldPath = Path;
#pragma warning restore CS0618 #pragma warning restore CS0618
if (oldPath == null) return; if (oldPath == null)
return;
var libDir = libraryDirectory ?? GlobalConfig.LibraryDir; var libDir = libraryDirectory ?? GlobalConfig.LibraryDir;
// if old package Path is same as new library, return // if old package Path is same as new library, return
@ -129,7 +146,8 @@ public class InstalledPackage
} }
// Try using pure migration first // Try using pure migration first
if (TryPureMigratePath(libraryDirectory)) return; if (TryPureMigratePath(libraryDirectory))
return;
// If not, we need to move the package directory // If not, we need to move the package directory
var packageFolderName = new DirectoryInfo(oldPath).Name; var packageFolderName = new DirectoryInfo(oldPath).Name;
@ -144,7 +162,10 @@ public class InstalledPackage
var suffix = 2; var suffix = 2;
while (Directory.Exists(newPackagePath)) while (Directory.Exists(newPackagePath))
{ {
newPackagePath = System.IO.Path.Combine(newPackagesDir, $"{packageFolderName}-{suffix}"); newPackagePath = System.IO.Path.Combine(
newPackagesDir,
$"{packageFolderName}-{suffix}"
);
suffix++; suffix++;
} }
@ -168,8 +189,10 @@ public class InstalledPackage
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(null, obj))
if (ReferenceEquals(this, obj)) return true; return false;
if (ReferenceEquals(this, obj))
return true;
return obj.GetType() == this.GetType() && Equals((InstalledPackage)obj); return obj.GetType() == this.GetType() && Equals((InstalledPackage)obj);
} }
@ -177,4 +200,29 @@ public class InstalledPackage
{ {
return Id.GetHashCode(); return Id.GetHashCode();
} }
#pragma warning disable CS0618 // Type or member is obsolete
public void OnDeserialized()
{
// Handle version migration
if (Version != null)
return;
if (
string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion)
)
{
// release mode
Version = new InstalledPackageVersion { InstalledReleaseVersion = PackageVersion };
}
else if (!string.IsNullOrWhiteSpace(PackageVersion))
{
Version = new InstalledPackageVersion
{
InstalledBranch = InstalledBranch,
InstalledCommitSha = PackageVersion
};
}
}
#pragma warning restore CS0618 // Type or member is obsolete
} }

23
StabilityMatrix.Core/Models/InstalledPackageVersion.cs

@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models;
public class InstalledPackageVersion
{
public string? InstalledReleaseVersion { get; set; }
public string? InstalledBranch { get; set; }
public string? InstalledCommitSha { get; set; }
[JsonIgnore]
public bool IsReleaseMode => string.IsNullOrWhiteSpace(InstalledBranch);
[JsonIgnore]
public string DisplayVersion =>
(
IsReleaseMode
? InstalledReleaseVersion
: string.IsNullOrWhiteSpace(InstalledCommitSha)
? InstalledBranch
: $"{InstalledBranch}@{InstalledCommitSha[..7]}"
) ?? string.Empty;
}

33
StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs

@ -0,0 +1,33 @@
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.PackageModification;
public class AddInstalledPackageStep : IPackageStep
{
private readonly ISettingsManager settingsManager;
private readonly InstalledPackage newInstalledPackage;
public AddInstalledPackageStep(
ISettingsManager settingsManager,
InstalledPackage newInstalledPackage
)
{
this.settingsManager = settingsManager;
this.newInstalledPackage = newInstalledPackage;
}
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
if (!string.IsNullOrWhiteSpace(newInstalledPackage.DisplayName))
{
settingsManager.PackageInstallsInProgress.Remove(newInstalledPackage.DisplayName);
}
await using var transaction = settingsManager.BeginTransaction();
transaction.Settings.InstalledPackages.Add(newInstalledPackage);
transaction.Settings.ActiveInstalledPackageId = newInstalledPackage.Id;
}
public string ProgressTitle => $"{newInstalledPackage.DisplayName} Installed";
}

27
StabilityMatrix.Core/Models/PackageModification/DownloadPackageVersionStep.cs

@ -0,0 +1,27 @@
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.PackageModification;
public class DownloadPackageVersionStep : IPackageStep
{
private readonly BasePackage package;
private readonly string installPath;
private readonly DownloadPackageVersionOptions downloadOptions;
public DownloadPackageVersionStep(
BasePackage package,
string installPath,
DownloadPackageVersionOptions downloadOptions
)
{
this.package = package;
this.installPath = installPath;
this.downloadOptions = downloadOptions;
}
public Task ExecuteAsync(IProgress<ProgressReport>? progress = null) =>
package.DownloadPackage(installPath, downloadOptions, progress);
public string ProgressTitle => "Downloading package...";
}

14
StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs

@ -0,0 +1,14 @@
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.PackageModification;
public interface IPackageModificationRunner
{
Task ExecuteSteps(IReadOnlyList<IPackageStep> steps);
bool IsRunning { get; set; }
ProgressReport CurrentProgress { get; set; }
IPackageStep? CurrentStep { get; set; }
event EventHandler<ProgressReport>? ProgressChanged;
List<string> ConsoleOutput { get; }
Guid Id { get; }
}

33
StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs

@ -0,0 +1,33 @@
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Core.Models.PackageModification;
public class InstallPackageStep : IPackageStep
{
private readonly BasePackage package;
private readonly TorchVersion torchVersion;
private readonly string installPath;
public InstallPackageStep(BasePackage package, TorchVersion torchVersion, string installPath)
{
this.package = package;
this.torchVersion = torchVersion;
this.installPath = installPath;
}
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
void OnConsoleOutput(ProcessOutput output)
{
progress?.Report(new ProgressReport { IsIndeterminate = true, Message = output.Text });
}
await package
.InstallPackage(installPath, torchVersion, progress, OnConsoleOutput)
.ConfigureAwait(false);
}
public string ProgressTitle => "Installing package...";
}

43
StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs

@ -0,0 +1,43 @@
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.PackageModification;
public class PackageModificationRunner : IPackageModificationRunner
{
public async Task ExecuteSteps(IReadOnlyList<IPackageStep> steps)
{
IProgress<ProgressReport> progress = new Progress<ProgressReport>(report =>
{
CurrentProgress = report;
if (!string.IsNullOrWhiteSpace(report.Message))
{
ConsoleOutput.Add(report.Message);
}
OnProgressChanged(report);
});
IsRunning = true;
foreach (var step in steps)
{
CurrentStep = step;
await step.ExecuteAsync(progress).ConfigureAwait(false);
}
progress.Report(
new ProgressReport(1f, message: "Package Install Complete", isIndeterminate: false)
);
IsRunning = false;
}
public bool IsRunning { get; set; }
public ProgressReport CurrentProgress { get; set; }
public IPackageStep? CurrentStep { get; set; }
public List<string> ConsoleOutput { get; } = new();
public Guid Id { get; } = Guid.NewGuid();
public event EventHandler<ProgressReport>? ProgressChanged;
protected virtual void OnProgressChanged(ProgressReport e) => ProgressChanged?.Invoke(this, e);
}

9
StabilityMatrix.Core/Models/PackageModification/PackageStep.cs

@ -0,0 +1,9 @@
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.PackageModification;
public interface IPackageStep
{
Task ExecuteAsync(IProgress<ProgressReport>? progress = null);
string ProgressTitle { get; }
}

24
StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs

@ -0,0 +1,24 @@
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.PackageModification;
public class SetPackageInstallingStep : IPackageStep
{
private readonly ISettingsManager settingsManager;
private readonly string packageName;
public SetPackageInstallingStep(ISettingsManager settingsManager, string packageName)
{
this.settingsManager = settingsManager;
this.packageName = packageName;
}
public Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
settingsManager.PackageInstallsInProgress.Add(packageName);
return Task.CompletedTask;
}
public string ProgressTitle => "Starting Package Installation";
}

32
StabilityMatrix.Core/Models/PackageModification/SetupModelFoldersStep.cs

@ -0,0 +1,32 @@
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.PackageModification;
public class SetupModelFoldersStep : IPackageStep
{
private readonly BasePackage package;
private readonly SharedFolderMethod sharedFolderMethod;
private readonly string installPath;
public SetupModelFoldersStep(
BasePackage package,
SharedFolderMethod sharedFolderMethod,
string installPath
)
{
this.package = package;
this.sharedFolderMethod = sharedFolderMethod;
this.installPath = installPath;
}
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
progress?.Report(
new ProgressReport(-1f, "Setting up shared folder links...", isIndeterminate: true)
);
await package.SetupModelFolders(installPath, sharedFolderMethod).ConfigureAwait(false);
}
public string ProgressTitle => "Setting up shared folder links...";
}

44
StabilityMatrix.Core/Models/PackageModification/SetupPrerequisitesStep.cs

@ -0,0 +1,44 @@
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
namespace StabilityMatrix.Core.Models.PackageModification;
public class SetupPrerequisitesStep : IPackageStep
{
private readonly IPrerequisiteHelper prerequisiteHelper;
private readonly IPyRunner pyRunner;
public SetupPrerequisitesStep(IPrerequisiteHelper prerequisiteHelper, IPyRunner pyRunner)
{
this.prerequisiteHelper = prerequisiteHelper;
this.pyRunner = pyRunner;
}
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
// git, vcredist, etc...
await prerequisiteHelper.InstallAllIfNecessary(progress).ConfigureAwait(false);
// python stuff
if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled)
{
progress?.Report(
new ProgressReport(-1f, "Installing Python prerequisites...", isIndeterminate: true)
);
await pyRunner.Initialize().ConfigureAwait(false);
if (!PyRunner.PipInstalled)
{
await pyRunner.SetupPip().ConfigureAwait(false);
}
if (!PyRunner.VenvInstalled)
{
await pyRunner.InstallPackage("virtualenv").ConfigureAwait(false);
}
}
}
public string ProgressTitle => "Installing prerequisites...";
}

1
StabilityMatrix.Core/Models/PackageVersion.cs

@ -4,4 +4,5 @@ public record PackageVersion
{ {
public required string TagName { get; set; } public required string TagName { get; set; }
public string? ReleaseNotesMarkdown { get; set; } public string? ReleaseNotesMarkdown { get; set; }
public bool IsPrerelease { get; set; }
} }

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

@ -28,15 +28,19 @@ public class A3WebUI : BaseGitPackage
new("https://github.com/AUTOMATIC1111/stable-diffusion-webui/raw/master/screenshot.png"); new("https://github.com/AUTOMATIC1111/stable-diffusion-webui/raw/master/screenshot.png");
public string RelativeArgsDefinitionScriptPath => "modules.cmd_args"; public string RelativeArgsDefinitionScriptPath => "modules.cmd_args";
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public A3WebUI(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, public A3WebUI(
IPrerequisiteHelper prerequisiteHelper) : IGithubApiCache githubApi,
base(githubApi, settingsManager, downloadService, prerequisiteHelper) ISettingsManager settingsManager,
{ IDownloadService downloadService,
} IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
// From https://github.com/AUTOMATIC1111/stable-diffusion-webui/tree/master/models // From https://github.com/AUTOMATIC1111/stable-diffusion-webui/tree/master/models
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new() public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{ {
[SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" }, [SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" },
[SharedFolderType.ESRGAN] = new[] { "models/ESRGAN" }, [SharedFolderType.ESRGAN] = new[] { "models/ESRGAN" },
@ -54,7 +58,8 @@ public class A3WebUI : BaseGitPackage
}; };
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions => new() public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{ {
new() new()
{ {
@ -74,13 +79,16 @@ public class A3WebUI : BaseGitPackage
{ {
Name = "VRAM", Name = "VRAM",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{ {
Level.Low => "--lowvram", Level.Low => "--lowvram",
Level.Medium => "--medvram", Level.Medium => "--medvram",
_ => null _ => null
}, },
Options = new() { "--lowvram", "--medvram" } Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" }
}, },
new() new()
{ {
@ -121,64 +129,70 @@ public class A3WebUI : BaseGitPackage
LaunchOptionDefinition.Extras LaunchOptionDefinition.Extras
}; };
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
public override async Task<string> GetLatestVersion() public override async Task<string> GetLatestVersion()
{ {
var release = await GetLatestRelease().ConfigureAwait(false); var release = await GetLatestRelease().ConfigureAwait(false);
return release.TagName!; return release.TagName!;
} }
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null) public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
await UnzipPackage(progress); await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
// Setup venv // Setup venv
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv")); await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation; venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false); await venvRunner.Setup(true).ConfigureAwait(false);
// Install torch / xformers based on gpu info switch (torchVersion)
var gpus = HardwareHelper.IterGpuInfo().ToList(); {
if (gpus.Any(g => g.IsNvidia)) case TorchVersion.Cpu:
{ await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true)); break;
case TorchVersion.Cuda:
Logger.Info("Starting torch install (CUDA)..."); await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput) break;
.ConfigureAwait(false); case TorchVersion.Rocm:
await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
Logger.Info("Installing xformers..."); break;
await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false); case TorchVersion.DirectMl:
} await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput)
else if (HardwareHelper.PreferRocm())
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true));
await venvRunner.PipInstall("--upgrade pip wheel", OnConsoleOutput)
.ConfigureAwait(false);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm511, OnConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
} break;
else default:
{ throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null);
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true));
Logger.Info("Starting torch install (CPU)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput).ConfigureAwait(false);
} }
// Install requirements file // Install requirements file
progress?.Report(new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)); progress?.Report(
new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)
);
Logger.Info("Installing requirements_versions.txt"); Logger.Info("Installing requirements_versions.txt");
await venvRunner.PipInstall($"-r requirements_versions.txt", OnConsoleOutput).ConfigureAwait(false); await venvRunner
.PipInstall($"-r requirements_versions.txt", onConsoleOutput)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Installing Package Requirements", isIndeterminate: false)); progress?.Report(
new ProgressReport(1f, "Installing Package Requirements", isIndeterminate: false)
);
progress?.Report(new ProgressReport(-1f, "Updating configuration", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Updating configuration", isIndeterminate: true));
// Create and add {"show_progress_type": "TAESD"} to config.json // Create and add {"show_progress_type": "TAESD"} to config.json
// Only add if the file doesn't exist // Only add if the file doesn't exist
var configPath = Path.Combine(InstallLocation, "config.json"); var configPath = Path.Combine(installLocation, "config.json");
if (!File.Exists(configPath)) if (!File.Exists(configPath))
{ {
var config = new JsonObject { { "show_progress_type", "TAESD" } }; var config = new JsonObject { { "show_progress_type", "TAESD" } };
@ -188,13 +202,18 @@ public class A3WebUI : BaseGitPackage
progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false)); progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false));
} }
public override async Task RunPackage(string installedPackagePath, string command, string arguments) public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{ {
await SetupVenv(installedPackagePath).ConfigureAwait(false); await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s) void HandleConsoleOutput(ProcessOutput s)
{ {
OnConsoleOutput(s); onConsoleOutput?.Invoke(s);
if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase)) if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase))
return; return;
@ -212,4 +231,21 @@ public class A3WebUI : BaseGitPackage
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, OnExit); VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, OnExit);
} }
private async Task InstallRocmTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm511, onConsoleOutput)
.ConfigureAwait(false);
}
} }

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

@ -7,6 +7,7 @@ using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
@ -35,20 +36,35 @@ public abstract class BaseGitPackage : BasePackage
public override string GithubUrl => $"https://github.com/{Author}/{Name}"; public override string GithubUrl => $"https://github.com/{Author}/{Name}";
public override string DownloadLocation => public string DownloadLocation =>
Path.Combine(SettingsManager.LibraryDir, "Packages", $"{Name}.zip"); Path.Combine(SettingsManager.LibraryDir, "Packages", $"{Name}.zip");
public override string InstallLocation { get; set; } protected string GetDownloadUrl(DownloadPackageVersionOptions versionOptions)
{
if (!string.IsNullOrWhiteSpace(versionOptions.CommitHash))
{
return $"https://github.com/{Author}/{Name}/archive/{versionOptions.CommitHash}.zip";
}
protected string GetDownloadUrl(string tagName, bool isCommitHash = false) if (!string.IsNullOrWhiteSpace(versionOptions.VersionTag))
{ {
return isCommitHash return $"https://api.github.com/repos/{Author}/{Name}/zipball/{versionOptions.VersionTag}";
? $"https://github.com/{Author}/{Name}/archive/{tagName}.zip"
: $"https://api.github.com/repos/{Author}/{Name}/zipball/{tagName}";
} }
protected BaseGitPackage(IGithubApiCache githubApi, ISettingsManager settingsManager, if (!string.IsNullOrWhiteSpace(versionOptions.BranchName))
IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper) {
return $"https://api.github.com/repos/{Author}/{Name}/zipball/{versionOptions.BranchName}";
}
throw new Exception("No download URL available");
}
protected BaseGitPackage(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
{ {
GithubApi = githubApi; GithubApi = githubApi;
SettingsManager = settingsManager; SettingsManager = settingsManager;
@ -58,9 +74,7 @@ public abstract class BaseGitPackage : BasePackage
protected async Task<Release> GetLatestRelease(bool includePrerelease = false) protected async Task<Release> GetLatestRelease(bool includePrerelease = false)
{ {
var releases = await GithubApi var releases = await GithubApi.GetAllReleases(Author, Name).ConfigureAwait(false);
.GetAllReleases(Author, Name)
.ConfigureAwait(false);
return includePrerelease ? releases.First() : releases.First(x => !x.Prerelease); return includePrerelease ? releases.First() : releases.First(x => !x.Prerelease);
} }
@ -74,32 +88,41 @@ public abstract class BaseGitPackage : BasePackage
return GithubApi.GetAllReleases(Author, Name); return GithubApi.GetAllReleases(Author, Name);
} }
public override Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10) public override Task<IEnumerable<GitCommit>?> GetAllCommits(
string branch,
int page = 1,
int perPage = 10
)
{ {
return GithubApi.GetAllCommits(Author, Name, branch, page, perPage); return GithubApi.GetAllCommits(Author, Name, branch, page, perPage);
} }
public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) public override async Task<PackageVersionOptions> GetAllVersionOptions()
{
// Release mode
if (isReleaseMode)
{ {
var packageVersionOptions = new PackageVersionOptions();
var allReleases = await GetAllReleases().ConfigureAwait(false); var allReleases = await GetAllReleases().ConfigureAwait(false);
return allReleases.Where(r => r.Prerelease == false).Select(r => var releasesList = allReleases.ToList();
if (releasesList.Any())
{
packageVersionOptions.AvailableVersions = releasesList.Select(
r =>
new PackageVersion new PackageVersion
{ {
TagName = r.TagName!, TagName = r.TagName!,
ReleaseNotesMarkdown = r.Body ReleaseNotesMarkdown = r.Body,
}); IsPrerelease = r.Prerelease
}
);
} }
// Branch mode // Branch mode
var allBranches = await GetAllBranches().ConfigureAwait(false); var allBranches = await GetAllBranches().ConfigureAwait(false);
return allBranches.Select(b => new PackageVersion packageVersionOptions.AvailableBranches = allBranches.Select(
{ b => new PackageVersion { TagName = $"{b.Name}", ReleaseNotesMarkdown = string.Empty }
TagName = $"{b.Name}", );
ReleaseNotesMarkdown = string.Empty
}); return packageVersionOptions;
} }
/// <summary> /// <summary>
@ -109,7 +132,8 @@ public abstract class BaseGitPackage : BasePackage
public async Task<PyVenvRunner> SetupVenv( public async Task<PyVenvRunner> SetupVenv(
string installedPackagePath, string installedPackagePath,
string venvName = "venv", string venvName = "venv",
bool forceRecreate = false) bool forceRecreate = false
)
{ {
var venvPath = Path.Combine(installedPackagePath, venvName); var venvPath = Path.Combine(installedPackagePath, venvName);
if (VenvRunner != null) if (VenvRunner != null)
@ -132,16 +156,17 @@ public abstract class BaseGitPackage : BasePackage
public override async Task<IEnumerable<Release>> GetReleaseTags() public override async Task<IEnumerable<Release>> GetReleaseTags()
{ {
var allReleases = await GithubApi var allReleases = await GithubApi.GetAllReleases(Author, Name).ConfigureAwait(false);
.GetAllReleases(Author, Name)
.ConfigureAwait(false);
return allReleases; return allReleases;
} }
public override async Task<string> DownloadPackage(string version, bool isCommitHash, public override async Task DownloadPackage(
string? branch, IProgress<ProgressReport>? progress = null) string installLocation,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null
)
{ {
var downloadUrl = GetDownloadUrl(version, isCommitHash); var downloadUrl = GetDownloadUrl(versionOptions);
if (!Directory.Exists(DownloadLocation.Replace($"{Name}.zip", ""))) if (!Directory.Exists(DownloadLocation.Replace($"{Name}.zip", "")))
{ {
@ -153,18 +178,20 @@ public abstract class BaseGitPackage : BasePackage
.ConfigureAwait(false); .ConfigureAwait(false);
progress?.Report(new ProgressReport(100, message: "Download Complete")); progress?.Report(new ProgressReport(100, message: "Download Complete"));
return version;
} }
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null) public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
await UnzipPackage(progress).ConfigureAwait(false); await UnzipPackage(installLocation, progress).ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, $"{DisplayName} installed successfully"));
File.Delete(DownloadLocation); File.Delete(DownloadLocation);
} }
protected Task UnzipPackage(IProgress<ProgressReport>? progress = null) protected Task UnzipPackage(string installLocation, IProgress<ProgressReport>? progress = null)
{ {
using var zip = ZipFile.OpenRead(DownloadLocation); using var zip = ZipFile.OpenRead(DownloadLocation);
var zipDirName = string.Empty; var zipDirName = string.Empty;
@ -181,55 +208,57 @@ public abstract class BaseGitPackage : BasePackage
zipDirName = entry.FullName; zipDirName = entry.FullName;
} }
var folderPath = Path.Combine(InstallLocation, var folderPath = Path.Combine(
entry.FullName.Replace(zipDirName, string.Empty)); installLocation,
entry.FullName.Replace(zipDirName, string.Empty)
);
Directory.CreateDirectory(folderPath); Directory.CreateDirectory(folderPath);
continue; continue;
} }
var destinationPath = Path.GetFullPath(
var destinationPath = Path.GetFullPath(Path.Combine(InstallLocation, Path.Combine(installLocation, entry.FullName.Replace(zipDirName, string.Empty))
entry.FullName.Replace(zipDirName, string.Empty))); );
entry.ExtractToFile(destinationPath, true); entry.ExtractToFile(destinationPath, true);
progress?.Report(new ProgressReport(current: Convert.ToUInt64(currentEntry), progress?.Report(
total: Convert.ToUInt64(totalEntries))); new ProgressReport(
current: Convert.ToUInt64(currentEntry),
total: Convert.ToUInt64(totalEntries)
)
);
} }
progress?.Report(new ProgressReport(1f, $"{DisplayName} installed successfully"));
return Task.CompletedTask; return Task.CompletedTask;
} }
public override async Task<bool> CheckForUpdates(InstalledPackage package) public override async Task<bool> CheckForUpdates(InstalledPackage package)
{ {
var currentVersion = package.PackageVersion; var currentVersion = package.Version;
if (string.IsNullOrWhiteSpace(currentVersion)) if (currentVersion == null)
{ {
return false; return false;
} }
try try
{ {
if (string.IsNullOrWhiteSpace(package.InstalledBranch)) if (currentVersion.IsReleaseMode)
{ {
var latestVersion = await GetLatestVersion().ConfigureAwait(false); var latestVersion = await GetLatestVersion().ConfigureAwait(false);
UpdateAvailable = latestVersion != currentVersion; UpdateAvailable = latestVersion != currentVersion.InstalledReleaseVersion;
return latestVersion != currentVersion; return UpdateAvailable;
} }
else
{ var allCommits = (
var allCommits = (await GetAllCommits(package.InstalledBranch) await GetAllCommits(currentVersion.InstalledBranch).ConfigureAwait(false)
.ConfigureAwait(false))?.ToList(); )?.ToList();
if (allCommits == null || !allCommits.Any()) if (allCommits == null || !allCommits.Any())
{ {
Logger.Warn("No commits found for {Package}", package.PackageName); Logger.Warn("No commits found for {Package}", package.PackageName);
return false; return false;
} }
var latestCommitHash = allCommits.First().Sha; var latestCommitHash = allCommits.First().Sha;
return latestCommitHash != currentVersion; return latestCommitHash != currentVersion.InstalledCommitSha;
}
} }
catch (ApiException e) catch (ApiException e)
{ {
@ -238,23 +267,38 @@ public abstract class BaseGitPackage : BasePackage
} }
} }
public override async Task<string> Update(InstalledPackage installedPackage, public override async Task<InstalledPackageVersion> Update(
IProgress<ProgressReport>? progress = null, bool includePrerelease = false) InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
// Release mode if (installedPackage.Version == null)
if (string.IsNullOrWhiteSpace(installedPackage.InstalledBranch)) throw new NullReferenceException("Version is null");
if (installedPackage.Version.IsReleaseMode)
{ {
var releases = await GetAllReleases().ConfigureAwait(false); var releases = await GetAllReleases().ConfigureAwait(false);
var latestRelease = releases.First(x => includePrerelease || !x.Prerelease); var latestRelease = releases.First(x => includePrerelease || !x.Prerelease);
await DownloadPackage(latestRelease.TagName, false, null, progress)
await DownloadPackage(
installedPackage.FullPath,
new DownloadPackageVersionOptions { VersionTag = latestRelease.TagName },
progress
)
.ConfigureAwait(false);
await InstallPackage(installedPackage.FullPath, torchVersion, progress)
.ConfigureAwait(false); .ConfigureAwait(false);
await InstallPackage(progress).ConfigureAwait(false);
return latestRelease.TagName; return new InstalledPackageVersion { InstalledReleaseVersion = latestRelease.TagName };
} }
// Commit mode // Commit mode
var allCommits = await GetAllCommits( var allCommits = await GetAllCommits(installedPackage.Version.InstalledBranch)
installedPackage.InstalledBranch).ConfigureAwait(false); .ConfigureAwait(false);
var latestCommit = allCommits?.First(); var latestCommit = allCommits?.First();
if (latestCommit is null || string.IsNullOrEmpty(latestCommit.Sha)) if (latestCommit is null || string.IsNullOrEmpty(latestCommit.Sha))
@ -262,34 +306,58 @@ public abstract class BaseGitPackage : BasePackage
throw new Exception("No commits found for branch"); throw new Exception("No commits found for branch");
} }
await DownloadPackage(latestCommit.Sha, true, installedPackage.InstalledBranch, progress) await DownloadPackage(
installedPackage.FullPath,
new DownloadPackageVersionOptions { CommitHash = latestCommit.Sha },
progress
)
.ConfigureAwait(false); .ConfigureAwait(false);
await InstallPackage(progress).ConfigureAwait(false); await InstallPackage(installedPackage.FullPath, torchVersion, progress)
return latestCommit.Sha; .ConfigureAwait(false);
return new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.InstalledBranch,
InstalledCommitSha = latestCommit.Sha
};
} }
public override Task SetupModelFolders(DirectoryPath installDirectory) public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
if (SharedFolders is { } folders) if (sharedFolderMethod == SharedFolderMethod.Symlink && SharedFolders is { } folders)
{ {
StabilityMatrix.Core.Helper.SharedFolders StabilityMatrix.Core.Helper.SharedFolders.SetupLinks(
.SetupLinks(folders, SettingsManager.ModelsDirectory, installDirectory); folders,
SettingsManager.ModelsDirectory,
installDirectory
);
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
public override async Task UpdateModelFolders(DirectoryPath installDirectory) public override async Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
if (SharedFolders is not null) if (SharedFolders is not null && sharedFolderMethod == SharedFolderMethod.Symlink)
{ {
await StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(this, await StabilityMatrix.Core.Helper.SharedFolders
SettingsManager.ModelsDirectory, installDirectory).ConfigureAwait(false); .UpdateLinksForPackage(this, SettingsManager.ModelsDirectory, installDirectory)
.ConfigureAwait(false);
} }
} }
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
if (SharedFolders is not null) if (SharedFolders is not null && sharedFolderMethod == SharedFolderMethod.Symlink)
{ {
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(this, installDirectory); StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(this, installDirectory);
} }

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

@ -1,8 +1,10 @@
using Octokit; using Octokit;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
namespace StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Core.Models.Packages;
@ -34,13 +36,91 @@ public abstract class BasePackage
public virtual bool ShouldIgnoreReleases => false; public virtual bool ShouldIgnoreReleases => false;
public virtual bool UpdateAvailable { get; set; } public virtual bool UpdateAvailable { get; set; }
public abstract Task<string> DownloadPackage(string version, bool isCommitHash, string? branch, public abstract Task DownloadPackage(
IProgress<ProgressReport>? progress = null); string installLocation,
public abstract Task InstallPackage(IProgress<ProgressReport>? progress = null); DownloadPackageVersionOptions versionOptions,
public abstract Task RunPackage(string installedPackagePath, string command, string arguments); IProgress<ProgressReport>? progress1
public abstract Task SetupModelFolders(DirectoryPath installDirectory); );
public abstract Task UpdateModelFolders(DirectoryPath installDirectory);
public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory); public abstract Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
);
public abstract Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
);
public abstract Task<bool> CheckForUpdates(InstalledPackage package);
public abstract Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
);
public virtual IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[]
{
SharedFolderMethod.Symlink,
SharedFolderMethod.Configuration,
SharedFolderMethod.None
};
public abstract SharedFolderMethod RecommendedSharedFolderMethod { get; }
public abstract Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract IEnumerable<TorchVersion> AvailableTorchVersions { get; }
public virtual TorchVersion GetRecommendedTorchVersion()
{
// if there's only one AvailableTorchVersion, return that
if (AvailableTorchVersions.Count() == 1)
{
return AvailableTorchVersions.First();
}
if (HardwareHelper.HasNvidiaGpu() && AvailableTorchVersions.Contains(TorchVersion.Cuda))
{
return TorchVersion.Cuda;
}
if (HardwareHelper.PreferRocm() && AvailableTorchVersions.Contains(TorchVersion.Rocm))
{
return TorchVersion.Rocm;
}
if (
HardwareHelper.PreferDirectML()
&& AvailableTorchVersions.Contains(TorchVersion.DirectMl)
)
{
return TorchVersion.DirectMl;
}
return TorchVersion.Cpu;
}
/// <summary> /// <summary>
/// Shuts down the subprocess, canceling any pending streams. /// Shuts down the subprocess, canceling any pending streams.
@ -51,11 +131,6 @@ public abstract class BasePackage
/// Shuts down the process, returning a Task to wait for output EOF. /// Shuts down the process, returning a Task to wait for output EOF.
/// </summary> /// </summary>
public abstract Task WaitForShutdown(); public abstract Task WaitForShutdown();
public abstract Task<bool> CheckForUpdates(InstalledPackage package);
public abstract Task<string> Update(InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null, bool includePrerelease = false);
public abstract Task<IEnumerable<Release>> GetReleaseTags(); public abstract Task<IEnumerable<Release>> GetReleaseTags();
public abstract List<LaunchOptionDefinition> LaunchOptions { get; } public abstract List<LaunchOptionDefinition> LaunchOptions { get; }
@ -68,19 +143,69 @@ public abstract class BasePackage
public virtual Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; } public virtual Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Task<string> GetLatestVersion(); public abstract Task<string> GetLatestVersion();
public abstract Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true); public abstract Task<PackageVersionOptions> GetAllVersionOptions();
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10); public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(
string branch,
int page = 1,
int perPage = 10
);
public abstract Task<IEnumerable<Branch>> GetAllBranches(); public abstract Task<IEnumerable<Branch>> GetAllBranches();
public abstract Task<IEnumerable<Release>> GetAllReleases(); public abstract Task<IEnumerable<Release>> GetAllReleases();
public virtual string? DownloadLocation { get; }
public virtual string? InstallLocation { get; set; }
public event EventHandler<ProcessOutput>? ConsoleOutput;
public event EventHandler<int>? Exited; public event EventHandler<int>? Exited;
public event EventHandler<string>? StartupComplete; public event EventHandler<string>? StartupComplete;
public void OnConsoleOutput(ProcessOutput output) => ConsoleOutput?.Invoke(this, output);
public void OnExit(int exitCode) => Exited?.Invoke(this, exitCode); public void OnExit(int exitCode) => Exited?.Invoke(this, exitCode);
public void OnStartupComplete(string url) => StartupComplete?.Invoke(this, url); public void OnStartupComplete(string url) => StartupComplete?.Invoke(this, url);
public virtual PackageVersionType AvailableVersionTypes =>
ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
protected async Task InstallCudaTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true)
);
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, onConsoleOutput)
.ConfigureAwait(false);
await venvRunner.PipInstall("xformers", onConsoleOutput).ConfigureAwait(false);
}
protected async Task InstallDirectMlTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true)
);
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, onConsoleOutput)
.ConfigureAwait(false);
}
protected async Task InstallCpuTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true)
);
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, onConsoleOutput)
.ConfigureAwait(false);
}
} }

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

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Drawing;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NLog; using NLog;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -28,15 +29,20 @@ public class ComfyUI : BaseGitPackage
public override Uri PreviewImageUri => public override Uri PreviewImageUri =>
new("https://github.com/comfyanonymous/ComfyUI/raw/master/comfyui_screenshot.png"); new("https://github.com/comfyanonymous/ComfyUI/raw/master/comfyui_screenshot.png");
public override bool ShouldIgnoreReleases => true; public override bool ShouldIgnoreReleases => true;
public override SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Configuration;
public ComfyUI(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, public ComfyUI(
IPrerequisiteHelper prerequisiteHelper) : IGithubApiCache githubApi,
base(githubApi, settingsManager, downloadService, prerequisiteHelper) ISettingsManager settingsManager,
{ IDownloadService downloadService,
} IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
// https://github.com/comfyanonymous/ComfyUI/blob/master/folder_paths.py#L11 // https://github.com/comfyanonymous/ComfyUI/blob/master/folder_paths.py#L11
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new() public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{ {
[SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" }, [SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" },
[SharedFolderType.Diffusers] = new[] { "models/diffusers" }, [SharedFolderType.Diffusers] = new[] { "models/diffusers" },
@ -51,13 +57,17 @@ public class ComfyUI : BaseGitPackage
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" },
}; };
public override List<LaunchOptionDefinition> LaunchOptions => new List<LaunchOptionDefinition> public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{ {
new() new()
{ {
Name = "VRAM", Name = "VRAM",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{ {
Level.Low => "--lowvram", Level.Low => "--lowvram",
Level.Medium => "--normalvram", Level.Medium => "--normalvram",
@ -90,69 +100,92 @@ public class ComfyUI : BaseGitPackage
public override Task<string> GetLatestVersion() => Task.FromResult("master"); public override Task<string> GetLatestVersion() => Task.FromResult("master");
public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) public override IEnumerable<TorchVersion> AvailableTorchVersions =>
{ new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
var allBranches = await GetAllBranches().ConfigureAwait(false);
return allBranches.Select(b => new PackageVersion
{
TagName = $"{b.Name}",
ReleaseNotesMarkdown = string.Empty
});
}
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null) public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
await UnzipPackage(progress); await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true));
// Setup venv // Setup venv
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv")); await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation; venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false); await venvRunner.Setup(true).ConfigureAwait(false);
// Install torch / xformers based on gpu info // Install torch / xformers based on gpu info
var gpus = HardwareHelper.IterGpuInfo().ToList(); switch (torchVersion)
if (gpus.Any(g => g.IsNvidia))
{ {
progress?.Report(new ProgressReport(-1, "Installing PyTorch for CUDA", isIndeterminate: true)); case TorchVersion.Cpu:
await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
Logger.Info("Starting torch install (CUDA)..."); break;
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput) case TorchVersion.Cuda:
await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
case TorchVersion.Rocm:
await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
case TorchVersion.DirectMl:
await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
break;
default:
throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null);
}
// Install requirements file
progress?.Report(
new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)
);
Logger.Info("Installing requirements.txt");
await venvRunner.PipInstall($"-r requirements.txt", onConsoleOutput).ConfigureAwait(false);
Logger.Info("Installing xformers..."); progress?.Report(
await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false); new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)
);
}
private async Task AutoDetectAndInstallTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
{
var gpus = HardwareHelper.IterGpuInfo().ToList();
if (gpus.Any(g => g.IsNvidia))
{
await InstallCudaTorch(venvRunner, progress).ConfigureAwait(false);
} }
else if (HardwareHelper.PreferRocm()) else if (HardwareHelper.PreferRocm())
{ {
progress?.Report(new ProgressReport(-1, "Installing PyTorch for ROCm", isIndeterminate: true)); await InstallRocmTorch(venvRunner, progress).ConfigureAwait(false);
await venvRunner }
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, OnConsoleOutput) else if (HardwareHelper.PreferDirectML())
.ConfigureAwait(false); {
await InstallDirectMlTorch(venvRunner, progress).ConfigureAwait(false);
} }
else else
{ {
progress?.Report(new ProgressReport(-1, "Installing PyTorch for CPU", isIndeterminate: true)); await InstallCpuTorch(venvRunner, progress).ConfigureAwait(false);
Logger.Info("Starting torch install (CPU)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput)
.ConfigureAwait(false);
} }
// Install requirements file
progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true));
Logger.Info("Installing requirements.txt");
await venvRunner.PipInstall($"-r requirements.txt", OnConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false));
} }
public override async Task RunPackage(string installedPackagePath, string command, string arguments) public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{ {
await SetupVenv(installedPackagePath).ConfigureAwait(false); await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s) void HandleConsoleOutput(ProcessOutput s)
{ {
OnConsoleOutput(s); onConsoleOutput?.Invoke(s);
if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase)) if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase))
{ {
@ -174,14 +207,22 @@ public class ComfyUI : BaseGitPackage
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}"; var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner?.RunDetached( VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
args.TrimEnd(),
HandleConsoleOutput,
HandleExit);
} }
public override Task SetupModelFolders(DirectoryPath installDirectory) public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
switch (sharedFolderMethod)
{
case SharedFolderMethod.None:
return Task.CompletedTask;
case SharedFolderMethod.Symlink:
return base.SetupModelFolders(installDirectory, sharedFolderMethod);
}
var extraPathsYamlPath = installDirectory + "extra_model_paths.yaml"; var extraPathsYamlPath = installDirectory + "extra_model_paths.yaml";
var modelsDir = SettingsManager.ModelsDirectory; var modelsDir = SettingsManager.ModelsDirectory;
@ -197,7 +238,9 @@ public class ComfyUI : BaseGitPackage
File.WriteAllText(extraPathsYamlPath, string.Empty); File.WriteAllText(extraPathsYamlPath, string.Empty);
} }
var yaml = File.ReadAllText(extraPathsYamlPath); var yaml = File.ReadAllText(extraPathsYamlPath);
var comfyModelPaths = deserializer.Deserialize<ComfyModelPathsYaml>(yaml) ?? var comfyModelPaths =
deserializer.Deserialize<ComfyModelPathsYaml>(yaml)
??
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
// cuz it can actually be null lol // cuz it can actually be null lol
new ComfyModelPathsYaml(); new ComfyModelPathsYaml();
@ -205,11 +248,12 @@ public class ComfyUI : BaseGitPackage
comfyModelPaths.StabilityMatrix ??= new ComfyModelPathsYaml.SmData(); comfyModelPaths.StabilityMatrix ??= new ComfyModelPathsYaml.SmData();
comfyModelPaths.StabilityMatrix.Checkpoints = Path.Combine(modelsDir, "StableDiffusion"); comfyModelPaths.StabilityMatrix.Checkpoints = Path.Combine(modelsDir, "StableDiffusion");
comfyModelPaths.StabilityMatrix.Vae = Path.Combine(modelsDir, "VAE"); comfyModelPaths.StabilityMatrix.Vae = Path.Combine(modelsDir, "VAE");
comfyModelPaths.StabilityMatrix.Loras = $"{Path.Combine(modelsDir, "Lora")}\n" + comfyModelPaths.StabilityMatrix.Loras =
$"{Path.Combine(modelsDir, "LyCORIS")}"; $"{Path.Combine(modelsDir, "Lora")}\n" + $"{Path.Combine(modelsDir, "LyCORIS")}";
comfyModelPaths.StabilityMatrix.UpscaleModels = $"{Path.Combine(modelsDir, "ESRGAN")}\n" + comfyModelPaths.StabilityMatrix.UpscaleModels =
$"{Path.Combine(modelsDir, "RealESRGAN")}\n" + $"{Path.Combine(modelsDir, "ESRGAN")}\n"
$"{Path.Combine(modelsDir, "SwinIR")}"; + $"{Path.Combine(modelsDir, "RealESRGAN")}\n"
+ $"{Path.Combine(modelsDir, "SwinIR")}";
comfyModelPaths.StabilityMatrix.Embeddings = Path.Combine(modelsDir, "TextualInversion"); comfyModelPaths.StabilityMatrix.Embeddings = Path.Combine(modelsDir, "TextualInversion");
comfyModelPaths.StabilityMatrix.Hypernetworks = Path.Combine(modelsDir, "Hypernetwork"); comfyModelPaths.StabilityMatrix.Hypernetworks = Path.Combine(modelsDir, "Hypernetwork");
comfyModelPaths.StabilityMatrix.Controlnet = Path.Combine(modelsDir, "ControlNet"); comfyModelPaths.StabilityMatrix.Controlnet = Path.Combine(modelsDir, "ControlNet");
@ -227,11 +271,42 @@ public class ComfyUI : BaseGitPackage
return Task.CompletedTask; return Task.CompletedTask;
} }
public override Task UpdateModelFolders(DirectoryPath installDirectory) => public override Task UpdateModelFolders(
SetupModelFolders(installDirectory); DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) => SetupModelFolders(installDirectory, sharedFolderMethod);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) => public override Task RemoveModelFolderLinks(
Task.CompletedTask; DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
return sharedFolderMethod switch
{
SharedFolderMethod.Configuration => Task.CompletedTask,
SharedFolderMethod.None => Task.CompletedTask,
SharedFolderMethod.Symlink
=> base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
_ => Task.CompletedTask
};
}
private async Task InstallRocmTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, onConsoleOutput)
.ConfigureAwait(false);
}
public class ComfyModelPathsYaml public class ComfyModelPathsYaml
{ {

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

@ -1,17 +1,20 @@
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Core.Models.Packages;
public class DankDiffusion : BaseGitPackage public class DankDiffusion : BaseGitPackage
{ {
public DankDiffusion(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, public DankDiffusion(
IPrerequisiteHelper prerequisiteHelper) : IGithubApiCache githubApi,
base(githubApi, settingsManager, downloadService, prerequisiteHelper) ISettingsManager settingsManager,
{ IDownloadService downloadService,
} IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
public override string Name => "dank-diffusion"; public override string Name => "dank-diffusion";
public override string DisplayName { get; set; } = "Dank Diffusion"; public override string DisplayName { get; set; } = "Dank Diffusion";
@ -21,41 +24,51 @@ public class DankDiffusion : BaseGitPackage
"https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE"; "https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE";
public override string Blurb => "A dank interface for diffusion"; public override string Blurb => "A dank interface for diffusion";
public override string LaunchCommand => "test"; public override string LaunchCommand => "test";
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IReadOnlyList<string> ExtraLaunchCommands => new[] public override IReadOnlyList<string> ExtraLaunchCommands => new[] { "test-config", };
{
"test-config",
};
public override Uri PreviewImageUri { get; } public override Uri PreviewImageUri { get; }
public override Task RunPackage(string installedPackagePath, string command, string arguments) public override Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task SetupModelFolders(DirectoryPath installDirectory) public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task UpdateModelFolders(DirectoryPath installDirectory) public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override IEnumerable<TorchVersion> AvailableTorchVersions { get; }
public override List<LaunchOptionDefinition> LaunchOptions { get; } public override List<LaunchOptionDefinition> LaunchOptions { get; }
public override Task<string> GetLatestVersion()
{
throw new NotImplementedException();
}
public override Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) public override Task<string> GetLatestVersion()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

111
StabilityMatrix.Core/Models/Packages/Fooocus.cs

@ -10,11 +10,13 @@ namespace StabilityMatrix.Core.Models.Packages;
public class Fooocus : BaseGitPackage public class Fooocus : BaseGitPackage
{ {
public Fooocus(IGithubApiCache githubApi, ISettingsManager settingsManager, public Fooocus(
IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper) : base(githubApi, IGithubApiCache githubApi,
settingsManager, downloadService, prerequisiteHelper) ISettingsManager settingsManager,
{ IDownloadService downloadService,
} IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
public override string Name => "Fooocus"; public override string Name => "Fooocus";
public override string DisplayName { get; set; } = "Fooocus"; public override string DisplayName { get; set; } = "Fooocus";
@ -28,14 +30,44 @@ public class Fooocus : BaseGitPackage
public override string LaunchCommand => "launch.py"; public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri => public override Uri PreviewImageUri =>
new("https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png"); new(
"https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png"
);
public override List<LaunchOptionDefinition> LaunchOptions => new() public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
new LaunchOptionDefinition
{
Name = "Port",
Type = LaunchOptionType.String,
Description = "Sets the listen port",
Options = { "--port" }
},
new LaunchOptionDefinition
{ {
Name = "Share",
Type = LaunchOptionType.Bool,
Description = "Set whether to share on Gradio",
Options = { "--share" }
},
new LaunchOptionDefinition
{
Name = "Listen",
Type = LaunchOptionType.String,
Description = "Set the listen interface",
Options = { "--listen" }
},
LaunchOptionDefinition.Extras LaunchOptionDefinition.Extras
}; };
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new() public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{ {
[SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" }, [SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" },
[SharedFolderType.Diffusers] = new[] { "models/diffusers" }, [SharedFolderType.Diffusers] = new[] { "models/diffusers" },
@ -50,49 +82,71 @@ public class Fooocus : BaseGitPackage
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" } [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }
}; };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm };
public override async Task<string> GetLatestVersion() public override async Task<string> GetLatestVersion()
{ {
var release = await GetLatestRelease().ConfigureAwait(false); var release = await GetLatestRelease().ConfigureAwait(false);
return release.TagName!; return release.TagName!;
} }
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null) public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
await base.InstallPackage(progress).ConfigureAwait(false); await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
var venvRunner = await SetupVenv(InstallLocation, forceRecreate: true).ConfigureAwait(false); var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true));
var torchVersion = "cpu"; var torchVersionStr = "cpu";
var gpus = HardwareHelper.IterGpuInfo().ToList();
if (gpus.Any(g => g.IsNvidia)) switch (torchVersion)
{
torchVersion = "cu118";
}
else if (HardwareHelper.PreferRocm())
{ {
torchVersion = "rocm5.4.2"; case TorchVersion.Cuda:
torchVersionStr = "cu118";
break;
case TorchVersion.Rocm:
torchVersionStr = "rocm5.4.2";
break;
case TorchVersion.Cpu:
break;
default:
throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null);
} }
await venvRunner await venvRunner
.PipInstall( .PipInstall(
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersion}", $"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}",
OnConsoleOutput).ConfigureAwait(false); onConsoleOutput
)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing requirements...", progress?.Report(
isIndeterminate: true)); new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)
await venvRunner.PipInstall("-r requirements_versions.txt", OnConsoleOutput) );
await venvRunner
.PipInstall("-r requirements_versions.txt", onConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
public override async Task RunPackage(string installedPackagePath, string command, string arguments) public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{ {
await SetupVenv(installedPackagePath).ConfigureAwait(false); await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s) void HandleConsoleOutput(ProcessOutput s)
{ {
OnConsoleOutput(s); onConsoleOutput?.Invoke(s);
if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase)) if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase))
{ {
@ -114,9 +168,6 @@ public class Fooocus : BaseGitPackage
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}"; var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner?.RunDetached( VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
args.TrimEnd(),
HandleConsoleOutput,
HandleExit);
} }
} }

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

@ -21,13 +21,13 @@ public class InvokeAI : BaseGitPackage
public override string Author => "invoke-ai"; public override string Author => "invoke-ai";
public override string LicenseType => "Apache-2.0"; public override string LicenseType => "Apache-2.0";
public override string LicenseUrl => public override string LicenseUrl => "https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE";
"https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE";
public override string Blurb => "Professional Creative Tools for Stable Diffusion"; public override string Blurb => "Professional Creative Tools for Stable Diffusion";
public override string LaunchCommand => "invokeai-web"; public override string LaunchCommand => "invokeai-web";
public override IReadOnlyList<string> ExtraLaunchCommands => new[] public override IReadOnlyList<string> ExtraLaunchCommands =>
new[]
{ {
"invokeai-configure", "invokeai-configure",
"invokeai-merge", "invokeai-merge",
@ -38,30 +38,40 @@ public class InvokeAI : BaseGitPackage
"invokeai-update", "invokeai-update",
}; };
public override Uri PreviewImageUri => new( public override Uri PreviewImageUri =>
"https://raw.githubusercontent.com/invoke-ai/InvokeAI/main/docs/assets/canvas_preview.png"); new(
"https://raw.githubusercontent.com/invoke-ai/InvokeAI/main/docs/assets/canvas_preview.png"
);
public override bool ShouldIgnoreReleases => true; public override bool ShouldIgnoreReleases => true;
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public InvokeAI( public InvokeAI(
IGithubApiCache githubApi, IGithubApiCache githubApi,
ISettingsManager settingsManager, ISettingsManager settingsManager,
IDownloadService downloadService, IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) : IPrerequisiteHelper prerequisiteHelper
base(githubApi, settingsManager, downloadService, prerequisiteHelper) )
{ : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
}
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new() public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{ {
[SharedFolderType.StableDiffusion] = new[] { RelativeRootPath + "/autoimport/main" }, [SharedFolderType.StableDiffusion] = new[] { RelativeRootPath + "/autoimport/main" },
[SharedFolderType.Lora] = new[] { RelativeRootPath + "/autoimport/lora" }, [SharedFolderType.Lora] = new[] { RelativeRootPath + "/autoimport/lora" },
[SharedFolderType.TextualInversion] = new[] { RelativeRootPath + "/autoimport/embedding" }, [SharedFolderType.TextualInversion] = new[]
{
RelativeRootPath + "/autoimport/embedding"
},
[SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" }, [SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" },
}; };
// https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md // https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md
public override List<LaunchOptionDefinition> LaunchOptions => new List<LaunchOptionDefinition> public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{ {
new() new()
{ {
@ -80,8 +90,9 @@ public class InvokeAI : BaseGitPackage
new() new()
{ {
Name = "Allow Origins", Name = "Allow Origins",
Description = "List of host names or IP addresses that are allowed to connect to the " + Description =
"InvokeAI API in the format ['host1','host2',...]", "List of host names or IP addresses that are allowed to connect to the "
+ "InvokeAI API in the format ['host1','host2',...]",
Type = LaunchOptionType.String, Type = LaunchOptionType.String,
DefaultValue = "[]", DefaultValue = "[]",
Options = new List<string> { "--allow-origins" } Options = new List<string> { "--allow-origins" }
@ -114,90 +125,115 @@ public class InvokeAI : BaseGitPackage
public override Task<string> GetLatestVersion() => Task.FromResult("main"); public override Task<string> GetLatestVersion() => Task.FromResult("main");
public override Task<string> DownloadPackage(string version, bool isCommitHash, string? branch, public override IEnumerable<TorchVersion> AvailableTorchVersions =>
IProgress<ProgressReport>? progress = null) new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm, TorchVersion.Mps };
public override TorchVersion GetRecommendedTorchVersion()
{
if (Compat.IsMacOS && Compat.IsArm)
{
return TorchVersion.Mps;
}
return base.GetRecommendedTorchVersion();
}
public override Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions downloadOptions,
IProgress<ProgressReport>? progress = null
)
{ {
return Task.FromResult(version); return Task.CompletedTask;
} }
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null) public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
// Setup venv // Setup venv
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv")); await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation; venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false); await venvRunner.Setup(true).ConfigureAwait(false);
venvRunner.EnvironmentVariables = GetEnvVars(InstallLocation); venvRunner.EnvironmentVariables = GetEnvVars(installLocation);
var gpus = HardwareHelper.IterGpuInfo().ToList();
progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true));
var pipCommandArgs = var pipCommandArgs =
"InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu"; "InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu";
// If has Nvidia Gpu, install CUDA version switch (torchVersion)
if (gpus.Any(g => g.IsNvidia))
{ {
case TorchVersion.Cuda:
Logger.Info("Starting InvokeAI install (CUDA)..."); Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs = pipCommandArgs =
"InvokeAI[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117"; "InvokeAI[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117";
} break;
// For AMD, Install ROCm version
else if (HardwareHelper.PreferRocm()) case TorchVersion.Rocm:
{
Logger.Info("Starting InvokeAI install (ROCm)..."); Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs = pipCommandArgs =
"InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2"; "InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
} break;
// For Apple silicon, use MPS
else if (Compat.IsMacOS && Compat.IsArm) case TorchVersion.Mps:
{
Logger.Info("Starting InvokeAI install (MPS)..."); Logger.Info("Starting InvokeAI install (MPS)...");
pipCommandArgs = "InvokeAI --use-pep517"; pipCommandArgs = "InvokeAI --use-pep517";
} break;
// CPU Version
else
{
Logger.Info("Starting InvokeAI install (CPU)...");
} }
await venvRunner.PipInstall(pipCommandArgs, OnConsoleOutput).ConfigureAwait(false); await venvRunner.PipInstall(pipCommandArgs, onConsoleOutput).ConfigureAwait(false);
await venvRunner await venvRunner
.PipInstall("rich packaging python-dotenv", OnConsoleOutput) .PipInstall("rich packaging python-dotenv", onConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Configuring InvokeAI", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Configuring InvokeAI", isIndeterminate: true));
await RunInvokeCommand(InstallLocation, "invokeai-configure", "--yes --skip-sd-weights", await RunInvokeCommand(
false).ConfigureAwait(false); installLocation,
"invokeai-configure",
"--yes --skip-sd-weights",
false,
onConsoleOutput
)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false)); progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false));
} }
public override async Task<string> Update(InstalledPackage installedPackage, IProgress<ProgressReport>? progress = null, public override async Task<InstalledPackageVersion> Update(
bool includePrerelease = false) InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
if (installedPackage.FullPath is null) if (installedPackage.FullPath is null || installedPackage.Version is null)
{ {
throw new NullReferenceException("Installed package path is null"); throw new NullReferenceException("Installed package is missing Path and/or Version");
} }
await using var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath, "venv")); await using var venvRunner = new PyVenvRunner(
Path.Combine(installedPackage.FullPath, "venv")
);
venvRunner.WorkingDirectory = installedPackage.FullPath; venvRunner.WorkingDirectory = installedPackage.FullPath;
venvRunner.EnvironmentVariables = GetEnvVars(installedPackage.FullPath); venvRunner.EnvironmentVariables = GetEnvVars(installedPackage.FullPath);
var latestVersion = await GetUpdateVersion(installedPackage).ConfigureAwait(false); var latestVersion = await GetUpdateVersion(installedPackage).ConfigureAwait(false);
var isReleaseMode = string.IsNullOrWhiteSpace(installedPackage.InstalledBranch); var isReleaseMode = installedPackage.Version.IsReleaseMode;
var downloadUrl = isReleaseMode var downloadUrl = isReleaseMode
? $"https://github.com/invoke-ai/InvokeAI/archive/{latestVersion}.zip" ? $"https://github.com/invoke-ai/InvokeAI/archive/{latestVersion}.zip"
: $"https://github.com/invoke-ai/InvokeAI/archive/refs/heads/{installedPackage.InstalledBranch}.zip"; : $"https://github.com/invoke-ai/InvokeAI/archive/refs/heads/{installedPackage.Version.InstalledBranch}.zip";
var gpus = HardwareHelper.IterGpuInfo().ToList(); var gpus = HardwareHelper.IterGpuInfo().ToList();
@ -206,60 +242,75 @@ public class InvokeAI : BaseGitPackage
var pipCommandArgs = var pipCommandArgs =
$"\"InvokeAI @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu --upgrade"; $"\"InvokeAI @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu --upgrade";
// If has Nvidia Gpu, install CUDA version switch (torchVersion)
if (gpus.Any(g => g.IsNvidia))
{ {
// If has Nvidia Gpu, install CUDA version
case TorchVersion.Cuda:
Logger.Info("Starting InvokeAI install (CUDA)..."); Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs = pipCommandArgs =
$"\"InvokeAI[xformers] @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117 --upgrade"; $"\"InvokeAI[xformers] @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117 --upgrade";
} break;
// For AMD, Install ROCm version // For AMD, Install ROCm version
else if (gpus.Any(g => g.IsAmd)) case TorchVersion.Rocm:
{
Logger.Info("Starting InvokeAI install (ROCm)..."); Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs = pipCommandArgs =
$"\"InvokeAI @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2 --upgrade"; $"\"InvokeAI @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2 --upgrade";
} break;
case TorchVersion.Mps:
// For Apple silicon, use MPS // For Apple silicon, use MPS
else if (Compat.IsMacOS && Compat.IsArm)
{
Logger.Info("Starting InvokeAI install (MPS)..."); Logger.Info("Starting InvokeAI install (MPS)...");
pipCommandArgs = $"\"InvokeAI @ {downloadUrl}\" --use-pep517 --upgrade"; pipCommandArgs = $"\"InvokeAI @ {downloadUrl}\" --use-pep517 --upgrade";
} break;
// CPU Version
else
{
Logger.Info("Starting InvokeAI install (CPU)...");
} }
await venvRunner.PipInstall(pipCommandArgs, OnConsoleOutput).ConfigureAwait(false); await venvRunner.PipInstall(pipCommandArgs, onConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false)); progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false));
return latestVersion; return isReleaseMode
? new InstalledPackageVersion { InstalledReleaseVersion = latestVersion }
: new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.InstalledBranch,
InstalledCommitSha = latestVersion
};
} }
public override Task public override Task RunPackage(
RunPackage(string installedPackagePath, string command, string arguments) => string installedPackagePath,
RunInvokeCommand(installedPackagePath, command, arguments, true); string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
) => RunInvokeCommand(installedPackagePath, command, arguments, true, onConsoleOutput);
private async Task<string> GetUpdateVersion(InstalledPackage installedPackage, bool includePrerelease = false) private async Task<string> GetUpdateVersion(
InstalledPackage installedPackage,
bool includePrerelease = false
)
{ {
if (string.IsNullOrWhiteSpace(installedPackage.InstalledBranch)) if (installedPackage.Version == null)
throw new NullReferenceException("Installed package version is null");
if (installedPackage.Version.IsReleaseMode)
{ {
var releases = await GetAllReleases().ConfigureAwait(false); var releases = await GetAllReleases().ConfigureAwait(false);
var latestRelease = releases.First(x => includePrerelease || !x.Prerelease); var latestRelease = releases.First(x => includePrerelease || !x.Prerelease);
return latestRelease.TagName; return latestRelease.TagName;
} }
var allCommits = await GetAllCommits( var allCommits = await GetAllCommits(installedPackage.Version.InstalledBranch)
installedPackage.InstalledBranch).ConfigureAwait(false); .ConfigureAwait(false);
var latestCommit = allCommits.First(); var latestCommit = allCommits.First();
return latestCommit.Sha; return latestCommit.Sha;
} }
private async Task RunInvokeCommand(string installedPackagePath, string command, private async Task RunInvokeCommand(
string arguments, bool runDetached) string installedPackagePath,
string command,
string arguments,
bool runDetached,
Action<ProcessOutput>? onConsoleOutput
)
{ {
await SetupVenv(installedPackagePath).ConfigureAwait(false); await SetupVenv(installedPackagePath).ConfigureAwait(false);
@ -298,7 +349,7 @@ public class InvokeAI : BaseGitPackage
{ {
void HandleConsoleOutput(ProcessOutput s) void HandleConsoleOutput(ProcessOutput s)
{ {
OnConsoleOutput(s); onConsoleOutput?.Invoke(s);
if (!s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase)) if (!s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase))
return; return;
@ -312,16 +363,18 @@ public class InvokeAI : BaseGitPackage
OnStartupComplete(WebUrl); OnStartupComplete(WebUrl);
} }
VenvRunner.RunDetached($"-c \"{code}\" {arguments}".TrimEnd(), HandleConsoleOutput, OnExit); VenvRunner.RunDetached(
$"-c \"{code}\" {arguments}".TrimEnd(),
HandleConsoleOutput,
OnExit
);
} }
else else
{ {
var result = await VenvRunner.Run($"-c \"{code}\" {arguments}".TrimEnd()) var result = await VenvRunner
.Run($"-c \"{code}\" {arguments}".TrimEnd())
.ConfigureAwait(false); .ConfigureAwait(false);
OnConsoleOutput(new ProcessOutput onConsoleOutput?.Invoke(new ProcessOutput { Text = result.StandardOutput });
{
Text = result.StandardOutput
});
} }
} }

7
StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs

@ -0,0 +1,7 @@
namespace StabilityMatrix.Core.Models.Packages;
public class PackageVersionOptions
{
public IEnumerable<PackageVersion>? AvailableVersions { get; set; }
public IEnumerable<PackageVersion>? AvailableBranches { get; set; }
}

75
StabilityMatrix.Core/Models/Packages/UnknownPackage.cs

@ -2,6 +2,7 @@
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Core.Models.Packages;
@ -21,46 +22,70 @@ public class UnknownPackage : BasePackage
public override Uri PreviewImageUri => new(""); public override Uri PreviewImageUri => new("");
public override IReadOnlyList<string> ExtraLaunchCommands => new[] public override IReadOnlyList<string> ExtraLaunchCommands => new[] { "test-config", };
{
"test-config",
};
/// <inheritdoc /> public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override Task<string> DownloadPackage(string version, bool isCommitHash, string? branch, IProgress<ProgressReport>? progress = null)
public override Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress1
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task InstallPackage(IProgress<ProgressReport>? progress = null) public override Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task RunPackage(string installedPackagePath, string command, string arguments) public override Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task SetupModelFolders(DirectoryPath installDirectory) public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task UpdateModelFolders(DirectoryPath installDirectory) public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cuda, TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl };
/// <inheritdoc /> /// <inheritdoc />
public override void Shutdown() public override void Shutdown()
{ {
@ -80,26 +105,40 @@ public class UnknownPackage : BasePackage
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<string> Update(InstalledPackage installedPackage, IProgress<ProgressReport>? progress = null, public override Task<InstalledPackageVersion> Update(
bool includePrerelease = false) InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Release>> GetReleaseTags() => Task.FromResult(Enumerable.Empty<Release>()); public override Task<IEnumerable<Release>> GetReleaseTags() =>
Task.FromResult(Enumerable.Empty<Release>());
public override List<LaunchOptionDefinition> LaunchOptions => new(); public override List<LaunchOptionDefinition> LaunchOptions => new();
public override Task<string> GetLatestVersion() => Task.FromResult(string.Empty); public override Task<string> GetLatestVersion() => Task.FromResult(string.Empty);
public override Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) => Task.FromResult(Enumerable.Empty<PackageVersion>()); public override Task<PackageVersionOptions> GetAllVersionOptions() =>
Task.FromResult(new PackageVersionOptions());
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10) => Task.FromResult<IEnumerable<GitCommit>?>(null); public override Task<IEnumerable<GitCommit>?> GetAllCommits(
string branch,
int page = 1,
int perPage = 10
) => Task.FromResult<IEnumerable<GitCommit>?>(null);
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Branch>> GetAllBranches() => Task.FromResult(Enumerable.Empty<Branch>()); public override Task<IEnumerable<Branch>> GetAllBranches() =>
Task.FromResult(Enumerable.Empty<Branch>());
/// <inheritdoc /> /// <inheritdoc />
public override Task<IEnumerable<Release>> GetAllReleases() => Task.FromResult(Enumerable.Empty<Release>()); public override Task<IEnumerable<Release>> GetAllReleases() =>
Task.FromResult(Enumerable.Empty<Release>());
} }

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

@ -1,5 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NLog; using NLog;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -29,14 +31,22 @@ public class VladAutomatic : BaseGitPackage
new("https://github.com/vladmandic/automatic/raw/master/html/black-orange.jpg"); new("https://github.com/vladmandic/automatic/raw/master/html/black-orange.jpg");
public override bool ShouldIgnoreReleases => true; public override bool ShouldIgnoreReleases => true;
public VladAutomatic(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
IPrerequisiteHelper prerequisiteHelper) :
base(githubApi, settingsManager, downloadService, prerequisiteHelper) public override IEnumerable<TorchVersion> AvailableTorchVersions =>
{ new[] { TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl, TorchVersion.Cuda };
}
public VladAutomatic(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
// https://github.com/vladmandic/automatic/blob/master/modules/shared.py#L324 // https://github.com/vladmandic/automatic/blob/master/modules/shared.py#L324
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new() public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{ {
[SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" }, [SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" },
[SharedFolderType.Diffusers] = new[] { "models/Diffusers" }, [SharedFolderType.Diffusers] = new[] { "models/Diffusers" },
@ -58,7 +68,8 @@ public class VladAutomatic : BaseGitPackage
}; };
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions => new() public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{ {
new() new()
{ {
@ -78,7 +89,10 @@ public class VladAutomatic : BaseGitPackage
{ {
Name = "VRAM", Name = "VRAM",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{ {
Level.Low => "--lowvram", Level.Low => "--lowvram",
Level.Medium => "--medvram", Level.Medium => "--medvram",
@ -87,6 +101,12 @@ public class VladAutomatic : BaseGitPackage
Options = new() { "--lowvram", "--medvram" } Options = new() { "--lowvram", "--medvram" }
}, },
new() new()
{
Name = "Auto-Launch Web UI",
Type = LaunchOptionType.Bool,
Options = new() { "--autolaunch" }
},
new()
{ {
Name = "Force use of Intel OneAPI XPU backend", Name = "Force use of Intel OneAPI XPU backend",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
@ -138,80 +158,110 @@ public class VladAutomatic : BaseGitPackage
public override Task<string> GetLatestVersion() => Task.FromResult("master"); public override Task<string> GetLatestVersion() => Task.FromResult("master");
public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) public override async Task InstallPackage(
{ string installLocation,
var allBranches = await GetAllBranches(); TorchVersion torchVersion,
return allBranches.Select(b => new PackageVersion IProgress<ProgressReport>? progress = null,
{ Action<ProcessOutput>? onConsoleOutput = null
TagName = $"{b.Name}", )
ReleaseNotesMarkdown = string.Empty
});
}
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null)
{ {
progress?.Report(new ProgressReport(-1f, "Installing package...", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Installing package...", isIndeterminate: true));
// Setup venv // Setup venv
var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv")); var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation; venvRunner.WorkingDirectory = installLocation;
venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables; venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables;
await venvRunner.Setup(true).ConfigureAwait(false); await venvRunner.Setup(true).ConfigureAwait(false);
// Run initial install switch (torchVersion)
if (HardwareHelper.HasNvidiaGpu())
{ {
// CUDA // Run initial install
await venvRunner.CustomInstall("launch.py --use-cuda --debug --test", OnConsoleOutput) case TorchVersion.Cuda:
await venvRunner
.CustomInstall("launch.py --use-cuda --debug --test", onConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
} break;
else if (HardwareHelper.PreferRocm()) case TorchVersion.Rocm:
{ await venvRunner
// ROCm .CustomInstall("launch.py --use-rocm --debug --test", onConsoleOutput)
await venvRunner.CustomInstall("launch.py --use-rocm --debug --test", OnConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
} break;
else if (HardwareHelper.PreferDirectML()) case TorchVersion.DirectMl:
{ await venvRunner
// DirectML .CustomInstall("launch.py --use-directml --debug --test", onConsoleOutput)
await venvRunner.CustomInstall("launch.py --use-directml --debug --test", OnConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
} break;
else default:
{ // CPU
await venvRunner.CustomInstall("launch.py --debug --test", OnConsoleOutput) await venvRunner
.CustomInstall("launch.py --debug --test", onConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
break;
} }
progress?.Report(new ProgressReport(1, isIndeterminate: false)); progress?.Report(new ProgressReport(1f, isIndeterminate: false));
} }
public override async Task<string> DownloadPackage(string version, bool isCommitHash, public override async Task DownloadPackage(
string? branch, IProgress<ProgressReport>? progress = null) string installLocation,
{ DownloadPackageVersionOptions downloadOptions,
progress?.Report(new ProgressReport(-1f, message: "Downloading package...", IProgress<ProgressReport>? progress = null
isIndeterminate: true, type: ProgressType.Download)); )
{
var installDir = new DirectoryPath(InstallLocation); progress?.Report(
new ProgressReport(
-1f,
message: "Downloading package...",
isIndeterminate: true,
type: ProgressType.Download
)
);
var installDir = new DirectoryPath(installLocation);
installDir.Create(); installDir.Create();
if (!string.IsNullOrWhiteSpace(downloadOptions.CommitHash))
{
await PrerequisiteHelper await PrerequisiteHelper
.RunGit(installDir.Parent ?? "", "clone", "https://github.com/vladmandic/automatic", .RunGit(
installDir.Name).ConfigureAwait(false); installDir.Parent ?? "",
"clone",
await PrerequisiteHelper.RunGit( "https://github.com/vladmandic/automatic",
InstallLocation, "checkout", version).ConfigureAwait(false); installDir.Name
)
.ConfigureAwait(false);
return version; await PrerequisiteHelper
.RunGit(installLocation, "checkout", downloadOptions.CommitHash)
.ConfigureAwait(false);
}
else if (!string.IsNullOrWhiteSpace(downloadOptions.BranchName))
{
await PrerequisiteHelper
.RunGit(
installDir.Parent ?? "",
"clone",
"-b",
downloadOptions.BranchName,
"https://github.com/vladmandic/automatic",
installDir.Name
)
.ConfigureAwait(false);
}
} }
public override async Task RunPackage(string installedPackagePath, string command, string arguments) public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{ {
await SetupVenv(installedPackagePath).ConfigureAwait(false); await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s) void HandleConsoleOutput(ProcessOutput s)
{ {
OnConsoleOutput(s); onConsoleOutput?.Invoke(s);
if (s.Text.Contains("Running on local URL", StringComparison.OrdinalIgnoreCase)) if (s.Text.Contains("Running on local URL", StringComparison.OrdinalIgnoreCase))
{ {
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
@ -235,36 +285,51 @@ public class VladAutomatic : BaseGitPackage
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit); VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
} }
public override async Task<string> Update(InstalledPackage installedPackage, public override async Task<InstalledPackageVersion> Update(
IProgress<ProgressReport>? progress = null, bool includePrerelease = false) InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
if (installedPackage.InstalledBranch is null) if (installedPackage.Version is null)
{ {
throw new Exception("Installed branch is null"); throw new Exception("Version is null");
} }
progress?.Report(new ProgressReport(-1f, message: "Downloading package update...", progress?.Report(
isIndeterminate: true, type: ProgressType.Update)); new ProgressReport(
-1f,
message: "Downloading package update...",
isIndeterminate: true,
type: ProgressType.Update
)
);
await PrerequisiteHelper.RunGit(installedPackage.FullPath, "checkout", await PrerequisiteHelper
installedPackage.InstalledBranch).ConfigureAwait(false); .RunGit(installedPackage.FullPath, "checkout", installedPackage.Version.InstalledBranch)
.ConfigureAwait(false);
var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath!, "venv")); var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath!, "venv"));
venvRunner.WorkingDirectory = InstallLocation; venvRunner.WorkingDirectory = installedPackage.FullPath!;
venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables; venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables;
await venvRunner.CustomInstall("launch.py --upgrade --test", OnConsoleOutput) await venvRunner
.CustomInstall("launch.py --upgrade --test", onConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
try try
{ {
var output = var output = await PrerequisiteHelper
await PrerequisiteHelper
.GetGitOutput(installedPackage.FullPath, "rev-parse", "HEAD") .GetGitOutput(installedPackage.FullPath, "rev-parse", "HEAD")
.ConfigureAwait(false); .ConfigureAwait(false);
return output.Replace(Environment.NewLine, "").Replace("\n", ""); return new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.InstalledBranch,
InstalledCommitSha = output.Replace(Environment.NewLine, "").Replace("\n", "")
};
} }
catch (Exception e) catch (Exception e)
{ {
@ -272,10 +337,113 @@ public class VladAutomatic : BaseGitPackage
} }
finally finally
{ {
progress?.Report(new ProgressReport(1f, message: "Update Complete", isIndeterminate: false, progress?.Report(
type: ProgressType.Update)); new ProgressReport(
1f,
message: "Update Complete",
isIndeterminate: false,
type: ProgressType.Update
)
);
}
return new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.InstalledBranch
};
} }
return installedPackage.InstalledBranch; public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
switch (sharedFolderMethod)
{
case SharedFolderMethod.Symlink:
return base.SetupModelFolders(installDirectory, sharedFolderMethod);
case SharedFolderMethod.None:
return Task.CompletedTask;
} }
// Config option
var configJsonPath = installDirectory + "config.json";
var exists = File.Exists(configJsonPath);
JsonObject? configRoot;
if (exists)
{
var configJson = File.ReadAllText(configJsonPath);
try
{
configRoot = JsonSerializer.Deserialize<JsonObject>(configJson) ?? new JsonObject();
}
catch (JsonException e)
{
Logger.Error(e, "Error setting up Vlad shared model config");
return Task.CompletedTask;
}
}
else
{
configRoot = new JsonObject();
}
configRoot["ckpt_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "StableDiffusion");
configRoot["diffusers_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "Diffusers");
configRoot["vae_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "VAE");
configRoot["lora_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "Lora");
configRoot["lyco_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "LyCORIS");
configRoot["embeddings_dir"] = Path.Combine(
SettingsManager.ModelsDirectory,
"TextualInversion"
);
configRoot["hypernetwork_dir"] = Path.Combine(
SettingsManager.ModelsDirectory,
"Hypernetwork"
);
configRoot["codeformer_models_path"] = Path.Combine(
SettingsManager.ModelsDirectory,
"Codeformer"
);
configRoot["gfpgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "GFPGAN");
configRoot["bsrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "BSRGAN");
configRoot["esrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ESRGAN");
configRoot["realesrgan_models_path"] = Path.Combine(
SettingsManager.ModelsDirectory,
"RealESRGAN"
);
configRoot["scunet_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ScuNET");
configRoot["swinir_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "SwinIR");
configRoot["ldsr_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "LDSR");
configRoot["clip_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "CLIP");
configRoot["control_net_models_path"] = Path.Combine(
SettingsManager.ModelsDirectory,
"ControlNet"
);
var configJsonStr = JsonSerializer.Serialize(
configRoot,
new JsonSerializerOptions { WriteIndented = true }
);
File.WriteAllText(configJsonPath, configJsonStr);
return Task.CompletedTask;
}
public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) => SetupModelFolders(installDirectory, sharedFolderMethod);
public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) =>
sharedFolderMethod switch
{
SharedFolderMethod.Symlink
=> base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
SharedFolderMethod.None => Task.CompletedTask,
_ => Task.CompletedTask
};
} }

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

@ -19,8 +19,10 @@ public class VoltaML : BaseGitPackage
public override string Blurb => "Fast Stable Diffusion with support for AITemplate"; public override string Blurb => "Fast Stable Diffusion with support for AITemplate";
public override string LaunchCommand => "main.py"; public override string LaunchCommand => "main.py";
public override Uri PreviewImageUri => new( public override Uri PreviewImageUri =>
"https://github.com/LykosAI/StabilityMatrix/assets/13956642/d9a908ed-5665-41a5-a380-98458f4679a8"); new(
"https://github.com/LykosAI/StabilityMatrix/assets/13956642/d9a908ed-5665-41a5-a380-98458f4679a8"
);
// There are releases but the manager just downloads the latest commit anyways, // There are releases but the manager just downloads the latest commit anyways,
// so we'll just limit to commit mode to be more consistent // so we'll just limit to commit mode to be more consistent
@ -30,21 +32,29 @@ public class VoltaML : BaseGitPackage
IGithubApiCache githubApi, IGithubApiCache githubApi,
ISettingsManager settingsManager, ISettingsManager settingsManager,
IDownloadService downloadService, IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) : IPrerequisiteHelper prerequisiteHelper
base(githubApi, settingsManager, downloadService, prerequisiteHelper) )
{ : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
}
// https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L86 // https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L86
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new() public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{ {
[SharedFolderType.StableDiffusion] = new[] { "data/models" }, [SharedFolderType.StableDiffusion] = new[] { "data/models" },
[SharedFolderType.Lora] = new[] { "data/lora" }, [SharedFolderType.Lora] = new[] { "data/lora" },
[SharedFolderType.TextualInversion] = new[] { "data/textual-inversion" }, [SharedFolderType.TextualInversion] = new[] { "data/textual-inversion" },
}; };
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable<TorchVersion> AvailableTorchVersions => new[] { TorchVersion.None };
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
// https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L45 // https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L45
public override List<LaunchOptionDefinition> LaunchOptions => new List<LaunchOptionDefinition> public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{ {
new() new()
{ {
@ -123,27 +133,41 @@ public class VoltaML : BaseGitPackage
public override Task<string> GetLatestVersion() => Task.FromResult("main"); public override Task<string> GetLatestVersion() => Task.FromResult("main");
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null) public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{ {
await UnzipPackage(progress).ConfigureAwait(false); await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
// Setup venv // Setup venv
progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true));
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv")); await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation; venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false); await venvRunner.Setup(true).ConfigureAwait(false);
// Install requirements // Install requirements
progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)); progress?.Report(
new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)
);
await venvRunner await venvRunner
.PipInstall("rich packaging python-dotenv", OnConsoleOutput) .PipInstall("rich packaging python-dotenv", onConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)); progress?.Report(
new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)
);
} }
public override async Task RunPackage(string installedPackagePath, string command, string arguments) public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{ {
await SetupVenv(installedPackagePath).ConfigureAwait(false); await SetupVenv(installedPackagePath).ConfigureAwait(false);
@ -153,7 +177,7 @@ public class VoltaML : BaseGitPackage
void HandleConsoleOutput(ProcessOutput s) void HandleConsoleOutput(ProcessOutput s)
{ {
OnConsoleOutput(s); onConsoleOutput?.Invoke(s);
if (s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase)) if (s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase))
{ {

7
StabilityMatrix.Core/Models/Progress/ProgressItem.cs

@ -1,3 +1,8 @@
namespace StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.Progress;
public record ProgressItem(Guid ProgressId, string Name, ProgressReport Progress, bool Failed = false); public record ProgressItem(
Guid ProgressId,
string Name,
ProgressReport Progress,
bool Failed = false
);

8
StabilityMatrix.Core/Models/SharedFolderMethod.cs

@ -0,0 +1,8 @@
namespace StabilityMatrix.Core.Models;
public enum SharedFolderMethod
{
Symlink,
Configuration,
None
}

11
StabilityMatrix.Core/Models/TorchVersion.cs

@ -0,0 +1,11 @@
namespace StabilityMatrix.Core.Models;
public enum TorchVersion
{
Cuda,
Rocm,
DirectMl,
Cpu,
Mps,
None
}

10
StabilityMatrix.Core/Services/ISettingsManager.cs

@ -15,6 +15,7 @@ public interface ISettingsManager
string ModelsDirectory { get; } string ModelsDirectory { get; }
string DownloadsDirectory { get; } string DownloadsDirectory { get; }
Settings Settings { get; } Settings Settings { get; }
List<string> PackageInstallsInProgress { get; set; }
event EventHandler<string>? LibraryDirChanged; event EventHandler<string>? LibraryDirChanged;
event EventHandler<PropertyChangedEventArgs>? SettingsPropertyChanged; event EventHandler<PropertyChangedEventArgs>? SettingsPropertyChanged;
@ -37,12 +38,15 @@ public interface ISettingsManager
void RelayPropertyFor<T, TValue>( void RelayPropertyFor<T, TValue>(
T source, T source,
Expression<Func<T, TValue>> sourceProperty, Expression<Func<T, TValue>> sourceProperty,
Expression<Func<Settings, TValue>> settingsProperty) where T : INotifyPropertyChanged; Expression<Func<Settings, TValue>> settingsProperty
)
where T : INotifyPropertyChanged;
/// <inheritdoc /> /// <inheritdoc />
void RegisterPropertyChangedHandler<T>( void RegisterPropertyChangedHandler<T>(
Expression<Func<Settings, T>> settingsProperty, Expression<Func<Settings, T>> settingsProperty,
Action<T> onPropertyChanged); Action<T> onPropertyChanged
);
/// <summary> /// <summary>
/// Attempts to locate and set the library path /// Attempts to locate and set the library path
@ -76,7 +80,7 @@ public interface ISettingsManager
/// </summary> /// </summary>
void InsertPathExtensions(); void InsertPathExtensions();
void UpdatePackageVersionNumber(Guid id, string? newVersion); void UpdatePackageVersionNumber(Guid id, InstalledPackageVersion? newVersion);
void SetLastUpdateCheck(InstalledPackage package); void SetLastUpdateCheck(InstalledPackage package);
List<LaunchOption> GetLaunchArgs(Guid packageId); List<LaunchOption> GetLaunchArgs(Guid packageId);
void SaveLaunchArgs(Guid packageId, List<LaunchOption> launchArgs); void SaveLaunchArgs(Guid packageId, List<LaunchOption> launchArgs);

37
StabilityMatrix.Core/Services/ModelIndexService.cs

@ -38,7 +38,8 @@ public class ModelIndexService : IModelIndexService
return await liteDbContext.LocalModelFiles return await liteDbContext.LocalModelFiles
.Query() .Query()
.Where(m => m.SharedFolderType == type) .Where(m => m.SharedFolderType == type)
.ToArrayAsync().ConfigureAwait(false); .ToArrayAsync()
.ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -50,8 +51,7 @@ public class ModelIndexService : IModelIndexService
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
logger.LogInformation("Refreshing model index..."); logger.LogInformation("Refreshing model index...");
using var db using var db = await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
= await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
var localModelFiles = db.GetCollection<LocalModelFile>("LocalModelFiles")!; var localModelFiles = db.GetCollection<LocalModelFile>("LocalModelFiles")!;
@ -77,8 +77,10 @@ public class ModelIndexService : IModelIndexService
var relativePath = Path.GetRelativePath(modelsDir, file); var relativePath = Path.GetRelativePath(modelsDir, file);
// Get shared folder name // Get shared folder name
var sharedFolderName = relativePath.Split(Path.DirectorySeparatorChar, var sharedFolderName = relativePath.Split(
StringSplitOptions.RemoveEmptyEntries)[0]; Path.DirectorySeparatorChar,
StringSplitOptions.RemoveEmptyEntries
)[0];
// Convert to enum // Convert to enum
var sharedFolderType = Enum.Parse<SharedFolderType>(sharedFolderName, true); var sharedFolderType = Enum.Parse<SharedFolderType>(sharedFolderName, true);
@ -90,24 +92,31 @@ public class ModelIndexService : IModelIndexService
// Try to find a connected model info // Try to find a connected model info
var jsonPath = file.Directory!.JoinFile( var jsonPath = file.Directory!.JoinFile(
new FilePath(file.NameWithoutExtension, ".cm-info.json")); new FilePath(file.NameWithoutExtension, ".cm-info.json")
);
if (jsonPath.Exists) if (jsonPath.Exists)
{ {
var connectedModelInfo = ConnectedModelInfo.FromJson( var connectedModelInfo = ConnectedModelInfo.FromJson(
await jsonPath.ReadAllTextAsync().ConfigureAwait(false)); await jsonPath.ReadAllTextAsync().ConfigureAwait(false)
);
localModel.ConnectedModelInfo = connectedModelInfo; localModel.ConnectedModelInfo = connectedModelInfo;
} }
// Try to find a preview image // Try to find a preview image
var previewImagePath = LocalModelFile.SupportedImageExtensions var previewImagePath = LocalModelFile.SupportedImageExtensions
.Select(ext => file.Directory!.JoinFile($"{file.NameWithoutExtension}.preview{ext}") .Select(
).FirstOrDefault(path => path.Exists); ext => file.Directory!.JoinFile($"{file.NameWithoutExtension}.preview{ext}")
)
.FirstOrDefault(path => path.Exists);
if (previewImagePath != null) if (previewImagePath != null)
{ {
localModel.PreviewImageRelativePath = Path.GetRelativePath(modelsDir, previewImagePath); localModel.PreviewImageRelativePath = Path.GetRelativePath(
modelsDir,
previewImagePath
);
} }
// Insert into database // Insert into database
@ -126,8 +135,12 @@ public class ModelIndexService : IModelIndexService
var indexDuration = indexEnd - indexStart; var indexDuration = indexEnd - indexStart;
var dbDuration = stopwatch.Elapsed - indexDuration; var dbDuration = stopwatch.Elapsed - indexDuration;
logger.LogInformation("Model index refreshed with {Entries} entries, took {IndexDuration:F1}ms ({DbDuration:F1}ms db)", logger.LogInformation(
added, indexDuration.TotalMilliseconds, dbDuration.TotalMilliseconds); "Model index refreshed with {Entries} entries, took {IndexDuration:F1}ms ({DbDuration:F1}ms db)",
added,
indexDuration.TotalMilliseconds,
dbDuration.TotalMilliseconds
);
} }
/// <inheritdoc /> /// <inheritdoc />

164
StabilityMatrix.Core/Services/SettingsManager.cs

@ -20,9 +20,15 @@ public class SettingsManager : ISettingsManager
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static readonly ReaderWriterLockSlim FileLock = new(); private static readonly ReaderWriterLockSlim FileLock = new();
private static readonly string GlobalSettingsPath = Path.Combine(Compat.AppDataHome, "global.json"); private static readonly string GlobalSettingsPath = Path.Combine(
Compat.AppDataHome,
"global.json"
);
private readonly string? originalEnvPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process); private readonly string? originalEnvPath = Environment.GetEnvironmentVariable(
"PATH",
EnvironmentVariableTarget.Process
);
// Library properties // Library properties
public bool IsPortableMode { get; private set; } public bool IsPortableMode { get; private set; }
@ -50,6 +56,7 @@ public class SettingsManager : ISettingsManager
private string SettingsPath => Path.Combine(LibraryDir, "settings.json"); private string SettingsPath => Path.Combine(LibraryDir, "settings.json");
public string ModelsDirectory => Path.Combine(LibraryDir, "Models"); public string ModelsDirectory => Path.Combine(LibraryDir, "Models");
public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads"); public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads");
public List<string> PackageInstallsInProgress { get; set; } = new();
public Settings Settings { get; private set; } = new(); public Settings Settings { get; private set; } = new();
@ -81,7 +88,9 @@ public class SettingsManager : ISettingsManager
{ {
if (!IsLibraryDirSet) if (!IsLibraryDirSet)
{ {
throw new InvalidOperationException("LibraryDir not set when BeginTransaction was called"); throw new InvalidOperationException(
"LibraryDir not set when BeginTransaction was called"
);
} }
return new SettingsTransaction(this, SaveSettingsAsync); return new SettingsTransaction(this, SaveSettingsAsync);
} }
@ -109,14 +118,16 @@ public class SettingsManager : ISettingsManager
if (expression.Body is not MemberExpression memberExpression) if (expression.Body is not MemberExpression memberExpression)
{ {
throw new ArgumentException( throw new ArgumentException(
$"Expression must be a member expression, not {expression.Body.NodeType}"); $"Expression must be a member expression, not {expression.Body.NodeType}"
);
} }
var propertyInfo = memberExpression.Member as PropertyInfo; var propertyInfo = memberExpression.Member as PropertyInfo;
if (propertyInfo == null) if (propertyInfo == null)
{ {
throw new ArgumentException( throw new ArgumentException(
$"Expression member must be a property, not {memberExpression.Member.MemberType}"); $"Expression member must be a property, not {memberExpression.Member.MemberType}"
);
} }
var name = propertyInfo.Name; var name = propertyInfo.Name;
@ -133,7 +144,9 @@ public class SettingsManager : ISettingsManager
public void RelayPropertyFor<T, TValue>( public void RelayPropertyFor<T, TValue>(
T source, T source,
Expression<Func<T, TValue>> sourceProperty, Expression<Func<T, TValue>> sourceProperty,
Expression<Func<Settings, TValue>> settingsProperty) where T : INotifyPropertyChanged Expression<Func<Settings, TValue>> settingsProperty
)
where T : INotifyPropertyChanged
{ {
var sourceGetter = sourceProperty.Compile(); var sourceGetter = sourceProperty.Compile();
var (propertyName, assigner) = Expressions.GetAssigner(sourceProperty); var (propertyName, assigner) = Expressions.GetAssigner(sourceProperty);
@ -148,12 +161,16 @@ public class SettingsManager : ISettingsManager
// Update source when settings change // Update source when settings change
SettingsPropertyChanged += (_, args) => SettingsPropertyChanged += (_, args) =>
{ {
if (args.PropertyName != propertyName) return; if (args.PropertyName != propertyName)
return;
Logger.Trace( Logger.Trace(
"[RelayPropertyFor] " + "[RelayPropertyFor] "
"Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}", + "Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}",
targetPropertyName, sourceTypeName, propertyName); targetPropertyName,
sourceTypeName,
propertyName
);
sourceSetter(source, settingsGetter(Settings)); sourceSetter(source, settingsGetter(Settings));
}; };
@ -161,12 +178,16 @@ public class SettingsManager : ISettingsManager
// Set and Save settings when source changes // Set and Save settings when source changes
source.PropertyChanged += (_, args) => source.PropertyChanged += (_, args) =>
{ {
if (args.PropertyName != propertyName) return; if (args.PropertyName != propertyName)
return;
Logger.Trace( Logger.Trace(
"[RelayPropertyFor] " + "[RelayPropertyFor] "
"{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}", + "{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}",
sourceTypeName, propertyName, targetPropertyName); sourceTypeName,
propertyName,
targetPropertyName
);
settingsSetter(Settings, sourceGetter(source)); settingsSetter(Settings, sourceGetter(source));
SaveSettingsAsync().SafeFireAndForget(); SaveSettingsAsync().SafeFireAndForget();
@ -179,7 +200,8 @@ public class SettingsManager : ISettingsManager
/// <inheritdoc /> /// <inheritdoc />
public void RegisterPropertyChangedHandler<T>( public void RegisterPropertyChangedHandler<T>(
Expression<Func<Settings, T>> settingsProperty, Expression<Func<Settings, T>> settingsProperty,
Action<T> onPropertyChanged) Action<T> onPropertyChanged
)
{ {
var settingsGetter = settingsProperty.Compile(); var settingsGetter = settingsProperty.Compile();
var (propertyName, _) = Expressions.GetAssigner(settingsProperty); var (propertyName, _) = Expressions.GetAssigner(settingsProperty);
@ -187,7 +209,8 @@ public class SettingsManager : ISettingsManager
// Invoke handler when settings change // Invoke handler when settings change
SettingsPropertyChanged += (_, args) => SettingsPropertyChanged += (_, args) =>
{ {
if (args.PropertyName != propertyName) return; if (args.PropertyName != propertyName)
return;
onPropertyChanged(settingsGetter(Settings)); onPropertyChanged(settingsGetter(Settings));
}; };
@ -212,15 +235,18 @@ public class SettingsManager : ISettingsManager
// 2. Check %APPDATA%/StabilityMatrix/library.json // 2. Check %APPDATA%/StabilityMatrix/library.json
FilePath libraryJsonFile = Compat.AppDataHome + "library.json"; FilePath libraryJsonFile = Compat.AppDataHome + "library.json";
if (!libraryJsonFile.Exists) return false; if (!libraryJsonFile.Exists)
return false;
try try
{ {
var libraryJson = libraryJsonFile.ReadAllText(); var libraryJson = libraryJsonFile.ReadAllText();
var librarySettings = JsonSerializer.Deserialize<LibrarySettings>(libraryJson); var librarySettings = JsonSerializer.Deserialize<LibrarySettings>(libraryJson);
if (!string.IsNullOrWhiteSpace(librarySettings?.LibraryPath) if (
&& Directory.Exists(librarySettings?.LibraryPath)) !string.IsNullOrWhiteSpace(librarySettings?.LibraryPath)
&& Directory.Exists(librarySettings?.LibraryPath)
)
{ {
LibraryDir = librarySettings.LibraryPath; LibraryDir = librarySettings.LibraryPath;
SetStaticLibraryPaths(); SetStaticLibraryPaths();
@ -252,7 +278,10 @@ public class SettingsManager : ISettingsManager
var libraryJsonFile = Compat.AppDataHome.JoinFile("library.json"); var libraryJsonFile = Compat.AppDataHome.JoinFile("library.json");
var library = new LibrarySettings { LibraryPath = path }; var library = new LibrarySettings { LibraryPath = path };
var libraryJson = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true }); var libraryJson = JsonSerializer.Serialize(
library,
new JsonSerializerOptions { WriteIndented = true }
);
libraryJsonFile.WriteAllText(libraryJson); libraryJsonFile.WriteAllText(libraryJson);
// actually create the LibraryPath directory // actually create the LibraryPath directory
@ -280,17 +309,20 @@ public class SettingsManager : ISettingsManager
/// </summary> /// </summary>
public IEnumerable<InstalledPackage> GetOldInstalledPackages() public IEnumerable<InstalledPackage> GetOldInstalledPackages()
{ {
var oldSettingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), var oldSettingsPath = Path.Combine(
"StabilityMatrix", "settings.json"); Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StabilityMatrix",
"settings.json"
);
if (!File.Exists(oldSettingsPath)) if (!File.Exists(oldSettingsPath))
yield break; yield break;
var oldSettingsJson = File.ReadAllText(oldSettingsPath); var oldSettingsJson = File.ReadAllText(oldSettingsPath);
var oldSettings = JsonSerializer.Deserialize<Settings>(oldSettingsJson, new JsonSerializerOptions var oldSettings = JsonSerializer.Deserialize<Settings>(
{ oldSettingsJson,
Converters = { new JsonStringEnumConverter() } new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }
}); );
// Absolute paths are old formats requiring migration // Absolute paths are old formats requiring migration
#pragma warning disable CS0618 #pragma warning disable CS0618
@ -308,17 +340,20 @@ public class SettingsManager : ISettingsManager
public Guid GetOldActivePackageId() public Guid GetOldActivePackageId()
{ {
var oldSettingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), var oldSettingsPath = Path.Combine(
"StabilityMatrix", "settings.json"); Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StabilityMatrix",
"settings.json"
);
if (!File.Exists(oldSettingsPath)) if (!File.Exists(oldSettingsPath))
return default; return default;
var oldSettingsJson = File.ReadAllText(oldSettingsPath); var oldSettingsJson = File.ReadAllText(oldSettingsPath);
var oldSettings = JsonSerializer.Deserialize<Settings>(oldSettingsJson, new JsonSerializerOptions var oldSettings = JsonSerializer.Deserialize<Settings>(
{ oldSettingsJson,
Converters = { new JsonStringEnumConverter() } new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }
}); );
if (oldSettings == null) if (oldSettings == null)
return default; return default;
@ -343,7 +378,8 @@ public class SettingsManager : ISettingsManager
/// </summary> /// </summary>
public void InsertPathExtensions() public void InsertPathExtensions()
{ {
if (Settings.PathExtensions == null) return; if (Settings.PathExtensions == null)
return;
var toInsert = GetPathExtensionsAsString(); var toInsert = GetPathExtensionsAsString();
// Append the original path, if any // Append the original path, if any
if (originalEnvPath != null) if (originalEnvPath != null)
@ -353,7 +389,7 @@ public class SettingsManager : ISettingsManager
Environment.SetEnvironmentVariable("PATH", toInsert, EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable("PATH", toInsert, EnvironmentVariableTarget.Process);
} }
public void UpdatePackageVersionNumber(Guid id, string? newVersion) public void UpdatePackageVersionNumber(Guid id, InstalledPackageVersion? newVersion)
{ {
var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == id); var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == id);
if (package == null || newVersion == null) if (package == null || newVersion == null)
@ -361,17 +397,15 @@ public class SettingsManager : ISettingsManager
return; return;
} }
package.PackageVersion = newVersion; package.Version = newVersion;
package.DisplayVersion = string.IsNullOrWhiteSpace(package.InstalledBranch)
? newVersion
: $"{package.InstalledBranch}@{newVersion[..7]}";
SaveSettings(); SaveSettings();
} }
public void SetLastUpdateCheck(InstalledPackage package) public void SetLastUpdateCheck(InstalledPackage package)
{ {
var installedPackage = Settings.InstalledPackages.First(p => p.DisplayName == package.DisplayName); var installedPackage = Settings.InstalledPackages.First(
p => p.DisplayName == package.DisplayName
);
installedPackage.LastUpdateCheck = package.LastUpdateCheck; installedPackage.LastUpdateCheck = package.LastUpdateCheck;
installedPackage.UpdateAvailable = package.UpdateAvailable; installedPackage.UpdateAvailable = package.UpdateAvailable;
SaveSettings(); SaveSettings();
@ -399,9 +433,14 @@ public class SettingsManager : ISettingsManager
public string? GetActivePackageHost() public string? GetActivePackageHost()
{ {
var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId); var package = Settings.InstalledPackages.FirstOrDefault(
if (package == null) return null; x => x.Id == Settings.ActiveInstalledPackageId
var hostOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "host"); );
if (package == null)
return null;
var hostOption = package.LaunchArgs?.FirstOrDefault(
x => x.Name.ToLowerInvariant() == "host"
);
if (hostOption?.OptionValue != null) if (hostOption?.OptionValue != null)
{ {
return hostOption.OptionValue as string; return hostOption.OptionValue as string;
@ -411,9 +450,14 @@ public class SettingsManager : ISettingsManager
public string? GetActivePackagePort() public string? GetActivePackagePort()
{ {
var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId); var package = Settings.InstalledPackages.FirstOrDefault(
if (package == null) return null; x => x.Id == Settings.ActiveInstalledPackageId
var portOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "port"); );
if (package == null)
return null;
var portOption = package.LaunchArgs?.FirstOrDefault(
x => x.Name.ToLowerInvariant() == "port"
);
if (portOption?.OptionValue != null) if (portOption?.OptionValue != null)
{ {
return portOption.OptionValue as string; return portOption.OptionValue as string;
@ -438,7 +482,8 @@ public class SettingsManager : ISettingsManager
public bool IsSharedFolderCategoryVisible(SharedFolderType type) public bool IsSharedFolderCategoryVisible(SharedFolderType type)
{ {
// False for default // False for default
if (type == 0) return false; if (type == 0)
return false;
return Settings.SharedFolderVisibleCategories?.HasFlag(type) ?? false; return Settings.SharedFolderVisibleCategories?.HasFlag(type) ?? false;
} }
@ -476,10 +521,14 @@ public class SettingsManager : ISettingsManager
var modelHashes = new HashSet<string>(); var modelHashes = new HashSet<string>();
var sharedModelDirectory = Path.Combine(LibraryDir, "Models"); var sharedModelDirectory = Path.Combine(LibraryDir, "Models");
if (!Directory.Exists(sharedModelDirectory)) return; if (!Directory.Exists(sharedModelDirectory))
return;
var connectedModelJsons = Directory.GetFiles(sharedModelDirectory, "*.cm-info.json", var connectedModelJsons = Directory.GetFiles(
SearchOption.AllDirectories); sharedModelDirectory,
"*.cm-info.json",
SearchOption.AllDirectories
);
foreach (var jsonFile in connectedModelJsons) foreach (var jsonFile in connectedModelJsons)
{ {
var json = File.ReadAllText(jsonFile); var json = File.ReadAllText(jsonFile);
@ -519,9 +568,10 @@ public class SettingsManager : ISettingsManager
var modifiedDefaultSerializerOptions = var modifiedDefaultSerializerOptions =
SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions(); SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions();
modifiedDefaultSerializerOptions.Converters.Add(new JsonStringEnumConverter()); modifiedDefaultSerializerOptions.Converters.Add(new JsonStringEnumConverter());
Settings = Settings = JsonSerializer.Deserialize<Settings>(
JsonSerializer.Deserialize<Settings>(settingsContent, settingsContent,
modifiedDefaultSerializerOptions)!; modifiedDefaultSerializerOptions
)!;
} }
finally finally
{ {
@ -539,11 +589,14 @@ public class SettingsManager : ISettingsManager
File.Create(SettingsPath).Close(); File.Create(SettingsPath).Close();
} }
var json = JsonSerializer.Serialize(Settings, new JsonSerializerOptions var json = JsonSerializer.Serialize(
Settings,
new JsonSerializerOptions
{ {
WriteIndented = true, WriteIndented = true,
Converters = { new JsonStringEnumConverter() } Converters = { new JsonStringEnumConverter() }
}); }
);
File.WriteAllText(SettingsPath, json); File.WriteAllText(SettingsPath, json);
} }
finally finally
@ -557,4 +610,3 @@ public class SettingsManager : ISettingsManager
return Task.Run(SaveSettings); return Task.Run(SaveSettings);
} }
} }

34
StabilityMatrix.Core/Services/TrackedDownloadService.cs

@ -15,9 +15,12 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
private readonly IDownloadService downloadService; private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly ConcurrentDictionary<Guid, (TrackedDownload, FileStream)> downloads = new(); private readonly ConcurrentDictionary<
Guid,
(TrackedDownload Download, FileStream Stream)
> downloads = new();
public IEnumerable<TrackedDownload> Downloads => downloads.Values.Select(x => x.Item1); public IEnumerable<TrackedDownload> Downloads => downloads.Values.Select(x => x.Download);
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler<TrackedDownload>? DownloadAdded; public event EventHandler<TrackedDownload>? DownloadAdded;
@ -25,7 +28,8 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
public TrackedDownloadService( public TrackedDownloadService(
ILogger<TrackedDownloadService> logger, ILogger<TrackedDownloadService> logger,
IDownloadService downloadService, IDownloadService downloadService,
ISettingsManager settingsManager) ISettingsManager settingsManager
)
{ {
this.logger = logger; this.logger = logger;
this.downloadService = downloadService; this.downloadService = downloadService;
@ -36,7 +40,8 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
{ {
var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory); var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory);
// Ignore if not exist // Ignore if not exist
if (!downloadsDir.Exists) return; if (!downloadsDir.Exists)
return;
LoadInProgressDownloads(downloadsDir); LoadInProgressDownloads(downloadsDir);
}); });
@ -60,7 +65,11 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory); var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory);
downloadsDir.Create(); downloadsDir.Create();
var jsonFile = downloadsDir.JoinFile($"{download.Id}.json"); var jsonFile = downloadsDir.JoinFile($"{download.Id}.json");
var jsonFileStream = jsonFile.Info.Open(FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read); var jsonFileStream = jsonFile.Info.Open(
FileMode.CreateNew,
FileAccess.ReadWrite,
FileShare.Read
);
// Serialize to json // Serialize to json
var json = JsonSerializer.Serialize(download); var json = JsonSerializer.Serialize(download);
@ -118,7 +127,9 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
{ {
downloadInfo.Item2.Dispose(); downloadInfo.Item2.Dispose();
// Delete json file // Delete json file
new DirectoryPath(settingsManager.DownloadsDirectory).JoinFile($"{download.Id}.json").Delete(); new DirectoryPath(settingsManager.DownloadsDirectory)
.JoinFile($"{download.Id}.json")
.Delete();
logger.LogDebug("Removed download {Download}", download.FileName); logger.LogDebug("Removed download {Download}", download.FileName);
} }
} }
@ -159,11 +170,18 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
else if (download.ProgressState != ProgressState.Inactive) else if (download.ProgressState != ProgressState.Inactive)
{ {
// If the download is not inactive, skip it // If the download is not inactive, skip it
logger.LogWarning("Skipping download {Download} with state {State}", download.FileName, download.ProgressState); logger.LogWarning(
"Skipping download {Download} with state {State}",
download.FileName,
download.ProgressState
);
fileStream.Dispose(); fileStream.Dispose();
// Delete json file // Delete json file
logger.LogDebug("Deleting json file for {Download} with unsupported state", download.FileName); logger.LogDebug(
"Deleting json file for {Download} with unsupported state",
download.FileName
);
file.Delete(); file.Delete();
continue; continue;
} }

1
StabilityMatrix.Core/StabilityMatrix.Core.csproj

@ -40,4 +40,5 @@
<PackageReference Include="YamlDotNet" Version="13.1.1" /> <PackageReference Include="YamlDotNet" Version="13.1.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

Loading…
Cancel
Save