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
      .github/workflows/release.yml
  2. 2
      .husky/task-runner.json
  3. 7
      CHANGELOG.md
  4. 10
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs
  5. 16
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs
  6. 8
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs
  7. 10
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs
  8. 52
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs
  9. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs
  10. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs
  11. 7
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs
  12. 12
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs
  13. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs
  14. 13
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs
  15. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs
  16. 34
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs
  17. 28
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs
  18. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs
  19. 7
      StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs
  20. 11
      StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs
  21. 3
      StabilityMatrix.Avalonia/App.axaml
  22. 9
      StabilityMatrix.Avalonia/App.axaml.cs
  23. 149
      StabilityMatrix.Avalonia/Assets.cs
  24. 132
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  25. 223
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  26. 15
      StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
  27. 4
      StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
  28. 122
      StabilityMatrix.Avalonia/Program.cs
  29. 11
      StabilityMatrix.Avalonia/ViewModels/Base/ConsoleProgressViewModel.cs
  30. 26
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
  31. 3
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs
  32. 15
      StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs
  33. 199
      StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
  34. 3
      StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs
  35. 497
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
  36. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
  37. 123
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  38. 199
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
  39. 94
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
  40. 50
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  41. 78
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  42. 332
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  43. 62
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  44. 189
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  45. 67
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  46. 4
      StabilityMatrix.Avalonia/ViewModels/Progress/DownloadProgressItemViewModel.cs
  47. 78
      StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
  48. 6
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressItemViewModel.cs
  49. 72
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs
  50. 322
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  51. 439
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  52. 56
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
  53. 51
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs
  54. 16
      StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs
  55. 2
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  56. 7
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  57. 69
      StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml
  58. 3
      StabilityMatrix.Core/Database/ILiteDbContext.cs
  59. 115
      StabilityMatrix.Core/Database/LiteDbContext.cs
  60. 44
      StabilityMatrix.Core/Helper/EventManager.cs
  61. 81
      StabilityMatrix.Core/Helper/SharedFolders.cs
  62. 7
      StabilityMatrix.Core/Models/Database/GitCommit.cs
  63. 7
      StabilityMatrix.Core/Models/Database/LocalModelFile.cs
  64. 8
      StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs
  65. 75
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  66. 110
      StabilityMatrix.Core/Models/InstalledPackage.cs
  67. 23
      StabilityMatrix.Core/Models/InstalledPackageVersion.cs
  68. 33
      StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs
  69. 27
      StabilityMatrix.Core/Models/PackageModification/DownloadPackageVersionStep.cs
  70. 14
      StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs
  71. 33
      StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs
  72. 43
      StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs
  73. 9
      StabilityMatrix.Core/Models/PackageModification/PackageStep.cs
  74. 24
      StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs
  75. 32
      StabilityMatrix.Core/Models/PackageModification/SetupModelFoldersStep.cs
  76. 44
      StabilityMatrix.Core/Models/PackageModification/SetupPrerequisitesStep.cs
  77. 1
      StabilityMatrix.Core/Models/PackageVersion.cs
  78. 298
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  79. 276
      StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
  80. 177
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  81. 303
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  82. 55
      StabilityMatrix.Core/Models/Packages/DankDiffusion.cs
  83. 151
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  84. 387
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  85. 7
      StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs
  86. 81
      StabilityMatrix.Core/Models/Packages/UnknownPackage.cs
  87. 516
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  88. 240
      StabilityMatrix.Core/Models/Packages/VoltaML.cs
  89. 7
      StabilityMatrix.Core/Models/Progress/ProgressItem.cs
  90. 8
      StabilityMatrix.Core/Models/SharedFolderMethod.cs
  91. 11
      StabilityMatrix.Core/Models/TorchVersion.cs
  92. 2
      StabilityMatrix.Core/Services/IModelIndexService.cs
  93. 14
      StabilityMatrix.Core/Services/ISettingsManager.cs
  94. 71
      StabilityMatrix.Core/Services/ModelIndexService.cs
  95. 268
      StabilityMatrix.Core/Services/SettingsManager.cs
  96. 100
      StabilityMatrix.Core/Services/TrackedDownloadService.cs
  97. 1
      StabilityMatrix.Core/StabilityMatrix.Core.csproj

2
.github/workflows/release.yml

@ -197,4 +197,4 @@ jobs:
tag_name: v${{ github.event.inputs.version }}
body: ${{ steps.release_notes.outputs.release_notes }}
draft: ${{ github.event.inputs.github-release-draft == 'true' }}
prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }}
prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }}

2
.husky/task-runner.json

@ -11,7 +11,7 @@
"name": "Run xamlstyler",
"group": "pre-commit",
"command": "dotnet",
"args": [ "xstyler", "${staged}" ],
"args": [ "xstyler", "-f", "${staged}" ],
"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/),
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
### Fixed
- Fixed [#108](https://github.com/LykosAI/StabilityMatrix/issues/108) - (Linux) Fixed permission error on updates [#103](https://github.com/LykosAI/StabilityMatrix/pull/103)

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

@ -8,8 +8,7 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Controls;
public partial class LogViewerControl : UserControl
{
public LogViewerControl()
=> InitializeComponent();
public LogViewerControl() => InitializeComponent();
private ILogDataStoreImpl? vm;
private LogModel? item;
@ -17,7 +16,7 @@ public partial class LogViewerControl : UserControl
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is null)
return;
@ -45,8 +44,9 @@ public partial class LogViewerControl : UserControl
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
if (vm is null) return;
if (vm is null)
return;
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));
var sysDrawColor = (SysDrawColor)value!;
return new SolidColorBrush(Color.FromArgb(
sysDrawColor.A,
sysDrawColor.R,
sysDrawColor.G,
sysDrawColor.B));
return new SolidColorBrush(
Color.FromArgb(sysDrawColor.A, sysDrawColor.R, sysDrawColor.G, sysDrawColor.B)
);
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
public object ConvertBack(
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
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> new EventId(0, value?.ToString() ?? string.Empty);
public object ConvertBack(
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 void Emit(this ILogger logger, EventId eventId,
LogLevel logLevel, string message, Exception? exception = null, params object?[] args)
public static void Emit(
this ILogger logger,
EventId eventId,
LogLevel logLevel,
string message,
Exception? exception = null,
params object?[] args
)
{
if (logger is null)
return;

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

@ -6,42 +6,28 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
public class DataStoreLoggerConfiguration
{
#region Properties
public EventId EventId { get; set; }
public Dictionary<LogLevel, LogEntryColor> Colors { get; } = new()
{
[LogLevel.Trace] = new LogEntryColor
{
Foreground = Color.DarkGray
},
[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
{
Foreground = Color.White,
Background = Color.OrangeRed
},
[LogLevel.Critical] = new LogEntryColor
{
Foreground = Color.White,
Background = Color.Red
},
[LogLevel.None] = new LogEntryColor
public Dictionary<LogLevel, LogEntryColor> Colors { get; } =
new()
{
Foreground = Color.Magenta
}
};
[LogLevel.Trace] = new LogEntryColor { Foreground = Color.DarkGray },
[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
{
Foreground = Color.White,
Background = Color.OrangeRed
},
[LogLevel.Critical] = new LogEntryColor
{
Foreground = Color.White,
Background = Color.Red
},
[LogLevel.None] = new LogEntryColor { Foreground = Color.Magenta }
};
#endregion
}

2
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs

@ -3,4 +3,4 @@
public interface ILogDataStoreImpl
{
public ILogDataStore DataStore { get; }
}
}

2
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs

@ -6,7 +6,7 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
public class LogDataStore : ILogDataStore
{
public static LogDataStore Instance { get; } = new();
#region Fields
private static readonly SemaphoreSlim _semaphore = new(initialCount: 1);

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

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

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

@ -13,11 +13,11 @@ public class LogModel
public EventId EventId { get; set; }
public object? State { get; set; }
public string? LoggerName { get; set; }
public string? CallerClassName { get; set; }
public string? CallerMemberName { get; set; }
public string? Exception { get; set; }
@ -26,8 +26,6 @@ public class LogModel
#endregion
public string LoggerDisplayName =>
LoggerName?
.Split('.', StringSplitOptions.RemoveEmptyEntries)
.LastOrDefault() ?? "";
public string LoggerDisplayName =>
LoggerName?.Split('.', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? "";
}

2
StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs

@ -18,4 +18,4 @@ public class LogViewerControlViewModel : ViewModel, ILogDataStoreImpl
public ILogDataStore DataStore { get; set; }
#endregion
}
}

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

@ -5,9 +5,14 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
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;
OnPropertyChanged(propertyName);
@ -16,6 +21,6 @@ public class ObservableObject : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
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;
public class ViewModel : ObservableObject { /* skip */ }
public class ViewModel
: ObservableObject { /* skip */
}

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

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

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

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

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

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

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

@ -6,15 +6,14 @@ namespace StabilityMatrix.Avalonia.Diagnostics.ViewModels;
public class LogWindowViewModel
{
public LogViewerControlViewModel LogViewer { get; }
public LogWindowViewModel(LogViewerControlViewModel logViewer)
{
LogViewer = logViewer;
}
public static LogWindowViewModel FromServiceProvider(IServiceProvider services)
{
return new LogWindowViewModel(
services.GetRequiredService<LogViewerControlViewModel>());
return new LogWindowViewModel(services.GetRequiredService<LogViewerControlViewModel>());
}
}

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

@ -16,13 +16,18 @@ public partial class LogWindow : Window
{
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(
KeyDownEvent,
PreviewKeyDown,
RoutingStrategies.Tunnel);
RoutingStrategies.Tunnel
);
void PreviewKeyDown(object? sender, KeyEventArgs e)
{

3
StabilityMatrix.Avalonia/App.axaml

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

149
StabilityMatrix.Avalonia/Assets.cs

@ -12,78 +12,133 @@ internal static class Assets
{
public static AvaloniaResource AppIcon { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico");
public static AvaloniaResource AppIconPng { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/Icon.png");
/// <summary>
/// Fixed image for models with no images.
/// </summary>
public static Uri NoImage { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/noimage.png");
public static AvaloniaResource LicensesJson => new(
"avares://StabilityMatrix.Avalonia/Assets/licenses.json");
private const UnixFileMode unix755 = UnixFileMode.UserRead | UnixFileMode.UserWrite |
UnixFileMode.UserExecute | UnixFileMode.GroupRead |
UnixFileMode.GroupExecute | UnixFileMode.OtherRead |
UnixFileMode.OtherExecute;
public static AvaloniaResource LicensesJson =>
new("avares://StabilityMatrix.Avalonia/Assets/licenses.json");
private const UnixFileMode unix755 =
UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
| UnixFileMode.GroupRead
| UnixFileMode.GroupExecute
| UnixFileMode.OtherRead
| UnixFileMode.OtherExecute;
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipExecutable => Compat.Switch(
(PlatformKind.Windows,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")),
(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)));
public static AvaloniaResource SevenZipExecutable =>
Compat.Switch(
(
PlatformKind.Windows,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")
),
(
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("linux")]
[SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipLicense => Compat.Switch(
(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 SevenZipLicense =>
Compat.Switch(
(
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("avares://StabilityMatrix.Avalonia/Assets/sitecustomize.py");
public static AvaloniaResource PyScriptSiteCustomize => new(
"avares://StabilityMatrix.Avalonia/Assets/sitecustomize.py");
[SupportedOSPlatform("windows")]
public static AvaloniaResource PyScriptGetPip => new(
"avares://StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc");
public static AvaloniaResource PyScriptGetPip =>
new("avares://StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc");
[SupportedOSPlatform("windows")]
public static IEnumerable<(AvaloniaResource resource, string relativePath)> PyModuleVenv =>
public static IEnumerable<(AvaloniaResource resource, string relativePath)> PyModuleVenv =>
FindAssets("win-x64/venv/");
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public static RemoteResource PythonDownloadUrl => Compat.Switch(
(PlatformKind.Windows | PlatformKind.X64, new RemoteResource(
new Uri("https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"),
"608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d")),
(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; } =
new("https://discord.com/invite/TUrgfECxHz");
public static Uri PatreonUrl { get; } =
new("https://patreon.com/StabilityMatrix");
public static RemoteResource PythonDownloadUrl =>
Compat.Switch(
(
PlatformKind.Windows | PlatformKind.X64,
new RemoteResource(
new Uri(
"https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"
),
"608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d"
)
),
(
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; } = new("https://discord.com/invite/TUrgfECxHz");
public static Uri PatreonUrl { get; } = new("https://patreon.com/StabilityMatrix");
/// <summary>
/// Yield AvaloniaResources given a relative directory path within the 'Assets' folder.
/// </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 targetUri = new Uri(baseUri, relativeAssetPath);

132
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -20,54 +20,64 @@ public class BetterContentDialog : ContentDialog
#region Reflection Shenanigans for setting content dialog result
[NotNull]
protected static readonly FieldInfo? ResultField = typeof(ContentDialog).GetField(
"_result",BindingFlags.Instance | BindingFlags.NonPublic);
"_result",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected ContentDialogResult Result
{
get => (ContentDialogResult) ResultField.GetValue(this)!;
get => (ContentDialogResult)ResultField.GetValue(this)!;
set => ResultField.SetValue(this, value);
}
[NotNull]
protected static readonly MethodInfo? HideCoreMethod = typeof(ContentDialog).GetMethod(
"HideCore", BindingFlags.Instance | BindingFlags.NonPublic);
"HideCore",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected void HideCore()
{
HideCoreMethod.Invoke(this, null);
}
// Also get button properties to hide on command execution change
[NotNull]
protected static readonly FieldInfo? PrimaryButtonField = typeof(ContentDialog).GetField(
"_primaryButton", BindingFlags.Instance | BindingFlags.NonPublic);
"_primaryButton",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected Button? PrimaryButton
{
get => (Button?) PrimaryButtonField.GetValue(this)!;
get => (Button?)PrimaryButtonField.GetValue(this)!;
set => PrimaryButtonField.SetValue(this, value);
}
[NotNull]
protected static readonly FieldInfo? SecondaryButtonField = typeof(ContentDialog).GetField(
"_secondaryButton", BindingFlags.Instance | BindingFlags.NonPublic);
protected Button? SecondaryButton
"_secondaryButton",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected Button? SecondaryButton
{
get => (Button?) SecondaryButtonField.GetValue(this)!;
get => (Button?)SecondaryButtonField.GetValue(this)!;
set => SecondaryButtonField.SetValue(this, value);
}
[NotNull]
protected static readonly FieldInfo? CloseButtonField = typeof(ContentDialog).GetField(
"_closeButton", BindingFlags.Instance | BindingFlags.NonPublic);
protected Button? CloseButton
"_closeButton",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected Button? CloseButton
{
get => (Button?) CloseButtonField.GetValue(this)!;
get => (Button?)CloseButtonField.GetValue(this)!;
set => CloseButtonField.SetValue(this, value);
}
static BetterContentDialog()
{
if (ResultField is null)
@ -84,11 +94,13 @@ public class BetterContentDialog : ContentDialog
}
}
#endregion
protected override Type StyleKeyOverride { get; } = typeof(ContentDialog);
public static readonly StyledProperty<bool> IsFooterVisibleProperty = AvaloniaProperty.Register<BetterContentDialog, bool>(
"IsFooterVisible", true);
public static readonly StyledProperty<bool> IsFooterVisibleProperty = AvaloniaProperty.Register<
BetterContentDialog,
bool
>("IsFooterVisible", true);
public bool IsFooterVisible
{
@ -96,9 +108,11 @@ public class BetterContentDialog : ContentDialog
set => SetValue(IsFooterVisibleProperty, value);
}
public static readonly StyledProperty<ScrollBarVisibility> ContentVerticalScrollBarVisibilityProperty
= AvaloniaProperty.Register<BetterContentDialog, ScrollBarVisibility>(
"ContentScrollBarVisibility", ScrollBarVisibility.Auto);
public static readonly StyledProperty<ScrollBarVisibility> ContentVerticalScrollBarVisibilityProperty =
AvaloniaProperty.Register<BetterContentDialog, ScrollBarVisibility>(
"ContentScrollBarVisibility",
ScrollBarVisibility.Auto
);
public ScrollBarVisibility ContentVerticalScrollBarVisibility
{
@ -106,17 +120,17 @@ public class BetterContentDialog : ContentDialog
set => SetValue(ContentVerticalScrollBarVisibilityProperty, value);
}
public static readonly StyledProperty<double> MinDialogWidthProperty = AvaloniaProperty.Register<BetterContentDialog, double>(
"MinDialogWidth");
public static readonly StyledProperty<double> MinDialogWidthProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MinDialogWidth");
public double MinDialogWidth
{
get => GetValue(MinDialogWidthProperty);
set => SetValue(MinDialogWidthProperty, value);
}
public static readonly StyledProperty<double> MaxDialogWidthProperty = AvaloniaProperty.Register<BetterContentDialog, double>(
"MaxDialogWidth");
public static readonly StyledProperty<double> MaxDialogWidthProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogWidth");
public double MaxDialogWidth
{
@ -124,8 +138,8 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogWidthProperty, value);
}
public static readonly StyledProperty<double> MaxDialogHeightProperty = AvaloniaProperty.Register<BetterContentDialog, double>(
"MaxDialogHeight");
public static readonly StyledProperty<double> MaxDialogHeightProperty =
AvaloniaProperty.Register<BetterContentDialog, double>("MaxDialogHeight");
public double MaxDialogHeight
{
@ -133,15 +147,14 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogHeightProperty, value);
}
public static readonly StyledProperty<Thickness> ContentMarginProperty = AvaloniaProperty.Register<BetterContentDialog, Thickness>(
"ContentMargin");
public static readonly StyledProperty<Thickness> ContentMarginProperty =
AvaloniaProperty.Register<BetterContentDialog, Thickness>("ContentMargin");
public Thickness ContentMargin
{
get => GetValue(ContentMarginProperty);
set => SetValue(ContentMarginProperty, value);
}
public BetterContentDialog()
{
@ -156,7 +169,16 @@ public class BetterContentDialog : ContentDialog
viewModel.SecondaryButtonClick += 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
// otherwise link visibility to IsEnabled
if (PrimaryButton is not null)
@ -170,10 +192,11 @@ public class BetterContentDialog : ContentDialog
}
else
{
PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
PrimaryButton.IsVisible =
IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
}
}
if (SecondaryButton is not null)
{
if (SecondaryButtonCommand is not null)
@ -185,10 +208,11 @@ public class BetterContentDialog : ContentDialog
}
else
{
SecondaryButton.IsVisible = IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText);
SecondaryButton.IsVisible =
IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText);
}
}
if (CloseButton is not null)
{
if (CloseButtonCommand is not null)
@ -210,7 +234,7 @@ public class BetterContentDialog : ContentDialog
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
TryBindButtons();
}
@ -236,7 +260,7 @@ public class BetterContentDialog : ContentDialog
var border = VisualChildren[0] as Border;
var panel = border?.Child as Panel;
var faBorder = panel?.Children[0] as FABorder;
// Set dialog bounds
if (MaxDialogWidth > 0)
{
@ -251,36 +275,38 @@ public class BetterContentDialog : ContentDialog
{
faBorder!.MaxHeight = MaxDialogHeight;
}
var border2 = faBorder?.Child as Border;
// 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 actualBorder = dialogSpaceGrid.Children[1] as Border;
// Get the parent border, which is what we want to hide
if (scrollViewer is null || actualBorder is null)
{
throw new InvalidOperationException("Could not find parent border");
}
var subBorder = scrollViewer.Content as Border;
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;
// Hide title if empty
if (Title is null or string {Length: 0})
if (Title is null or string { Length: 0 })
{
contentControlTitle!.IsVisible = false;
}
// Set footer and scrollbar visibility states
actualBorder.IsVisible = IsFooterVisible;
scrollViewer.VerticalScrollBarVisibility = ContentVerticalScrollBarVisibility;
// Also call the vm's OnLoad
if (Content is Control {DataContext: ViewModelBase viewModel})
if (Content is Control { DataContext: ViewModelBase viewModel })
{
viewModel.OnLoaded();
Dispatcher.UIThread.InvokeAsync(viewModel.OnLoadedAsync).SafeFireAndForget();

223
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -13,6 +13,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Helper;
@ -20,6 +21,7 @@ using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
@ -30,7 +32,8 @@ namespace StabilityMatrix.Avalonia.DesignData;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DesignData
{
[NotNull] public static IServiceProvider? Services { get; set; }
[NotNull]
public static IServiceProvider? Services { get; set; }
private static bool isInitialized;
@ -44,38 +47,49 @@ public static class DesignData
var services = new ServiceCollection();
var activePackageId = Guid.NewGuid();
services.AddSingleton<ISettingsManager, MockSettingsManager>(_ => new MockSettingsManager
{
Settings =
{
InstalledPackages = new List<InstalledPackage>
services.AddSingleton<ISettingsManager, MockSettingsManager>(
_ =>
new MockSettingsManager
{
new()
Settings =
{
Id = activePackageId,
DisplayName = "My Installed Package",
DisplayVersion = "v1.0.0",
PackageName = "stable-diffusion-webui",
PackageVersion = "v1.0.0",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
},
new()
{
Id = Guid.NewGuid(),
DisplayName = "Comfy Diffusion WebUI Dev Branch Long Name",
PackageName = "ComfyUI",
DisplayVersion = "main@ab73d4a",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
InstalledPackages = new List<InstalledPackage>
{
new()
{
Id = activePackageId,
DisplayName = "My Installed Package",
PackageName = "stable-diffusion-webui",
Version = new InstalledPackageVersion
{
InstalledReleaseVersion = "v1.0.0"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
},
new()
{
Id = Guid.NewGuid(),
DisplayName = "Comfy Diffusion WebUI Dev Branch Long Name",
PackageName = "ComfyUI",
Version = new InstalledPackageVersion
{
InstalledBranch = "master",
InstalledCommitSha =
"abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
}
},
ActiveInstalledPackageId = activePackageId
}
},
ActiveInstalledPackageId = activePackageId
}
});
}
);
// General services
services.AddLogging()
services
.AddLogging()
.AddSingleton<INavigationService, NavigationService>()
.AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<IUpdateHelper, UpdateHelper>()
@ -90,7 +104,8 @@ public static class DesignData
.AddSingleton<IHttpClientFactory, MockHttpClientFactory>()
.AddSingleton<IDiscordRichPresenceService, MockDiscordRichPresenceService>()
.AddSingleton<IModelIndexService, MockModelIndexService>()
.AddSingleton<ITrackedDownloadService, MockTrackedDownloadService>();
.AddSingleton<ITrackedDownloadService, MockTrackedDownloadService>()
.AddSingleton<IPackageModificationRunner, PackageModificationRunner>();
// Placeholder services that nobody should need during design time
services
@ -122,26 +137,31 @@ public static class DesignData
LaunchOptionsViewModel = Services.GetRequiredService<LaunchOptionsViewModel>();
LaunchOptionsViewModel.Cards = new[]
{
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition
{
Name = "Host",
Type = LaunchOptionType.String,
Description = "The host name for the Web UI",
DefaultValue = "localhost",
Options = {"--host"}
}),
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition
{
Name = "API",
Type = LaunchOptionType.Bool,
Options = {"--api"}
})
LaunchOptionCard.FromDefinition(
new LaunchOptionDefinition
{
Name = "Host",
Type = LaunchOptionType.String,
Description = "The host name for the Web UI",
DefaultValue = "localhost",
Options = { "--host" }
}
),
LaunchOptionCard.FromDefinition(
new LaunchOptionDefinition
{
Name = "API",
Type = LaunchOptionType.Bool,
Options = { "--api" }
}
)
};
LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages =
packageFactory.GetAllAvailablePackages().ToImmutableArray();
InstallerViewModel.AvailablePackages = packageFactory
.GetAllAvailablePackages()
.ToImmutableArray();
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
@ -158,8 +178,9 @@ public static class DesignData
{
FilePath = "~/Models/StableDiffusion/electricity-light.safetensors",
Title = "Auroral Background",
PreviewImagePath = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" +
"78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
PreviewImagePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ConnectedModel = new ConnectedModelInfo
{
VersionName = "Lightning Auroral",
@ -174,11 +195,7 @@ public static class DesignData
}
}
},
new()
{
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
},
new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" },
},
},
new(settingsManager, downloadService, modelFinder)
@ -200,11 +217,7 @@ public static class DesignData
},
CheckpointFiles = new AdvancedObservableList<CheckpointFile>
{
new()
{
FilePath = "~/Models/Lora/lora_v2.pt",
Title = "Best Lora v2",
}
new() { FilePath = "~/Models/Lora/lora_v2.pt", Title = "Best Lora v2", }
}
}
};
@ -214,8 +227,8 @@ public static class DesignData
folder.DisplayedCheckpointFiles = folder.CheckpointFiles;
}
CheckpointBrowserViewModel.ModelCards = new
ObservableCollection<CheckpointBrowserCardViewModel>
CheckpointBrowserViewModel.ModelCards =
new ObservableCollection<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
@ -233,8 +246,9 @@ public static class DesignData
{
FilePath = "~/Models/StableDiffusion/electricity-light.safetensors",
Title = "Auroral Background",
PreviewImagePath = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" +
"78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
PreviewImagePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ConnectedModel = new ConnectedModelInfo
{
VersionName = "Lightning Auroral",
@ -249,19 +263,28 @@ public static class DesignData
}
}
},
new()
{
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
}
new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }
};
ProgressManagerViewModel.ProgressItems.AddRange(new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(new ProgressItem(Guid.NewGuid(), "Test File.exe",
new ProgressReport(0.5f, "Downloading..."))),
new MockDownloadProgressItemViewModel("Test File 2.exe"),
});
ProgressManagerViewModel.ProgressItems.AddRange(
new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(
new ProgressItem(
Guid.NewGuid(),
"Test File.exe",
new ProgressReport(0.5f, "Downloading...")
)
),
new MockDownloadProgressItemViewModel("Test File 2.exe"),
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
)
}
);
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.UpdateText =
@ -272,9 +295,14 @@ public static class DesignData
isInitialized = true;
}
[NotNull] public static InstallerViewModel? InstallerViewModel { get; private set; }
[NotNull] public static LaunchOptionsViewModel? LaunchOptionsViewModel { get; private set; }
[NotNull] public static UpdateViewModel? UpdateViewModel { get; private set; }
[NotNull]
public static InstallerViewModel? InstallerViewModel { get; private set; }
[NotNull]
public static LaunchOptionsViewModel? LaunchOptionsViewModel { get; private set; }
[NotNull]
public static UpdateViewModel? UpdateViewModel { get; private set; }
public static ServiceManager<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>();
@ -296,13 +324,15 @@ public static class DesignData
var vm = Services.GetRequiredService<PackageManagerViewModel>();
vm.SetPackages(settings.Settings.InstalledPackages);
vm.SetUnknownPackages(new InstalledPackage[]
{
UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"),
});
vm.SetUnknownPackages(
new InstalledPackage[]
{
UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"),
}
);
vm.PackageCards[0].IsUpdateAvailable = true;
return vm;
}
}
@ -328,7 +358,8 @@ public static class DesignData
new()
{
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.
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",
@ -355,19 +386,18 @@ The gallery images are often inpainted, but you will get something very similar
Fp = CivitModelFpType.fp32,
Size = CivitModelSize.full
},
Hashes = new CivitFileHashes
{
BLAKE3 = "ABCD"
}
Hashes = new CivitFileHashes { BLAKE3 = "ABCD" }
}
}
}
};
var sampleViewModel =
new ModelVersionViewModel(new HashSet<string> {"ABCD"}, sampleCivitVersions[0]);
var sampleViewModel = new ModelVersionViewModel(
new HashSet<string> { "ABCD" },
sampleCivitVersions[0]
);
// Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel> {sampleViewModel};
vm.Versions = new List<ModelVersionViewModel> { sampleViewModel };
vm.Title = sampleCivitVersions[0].Name;
vm.Description = sampleCivitVersions[0].Description;
vm.SelectedVersionViewModel = 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>(
viewModel =>
public static EnvVarsViewModel EnvVarsViewModel =>
DialogFactory.Get<EnvVarsViewModel>(viewModel =>
{
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>
{
new("UWU", "TRUE"),
};
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair> { new("UWU", "TRUE"), };
});
public static PackageImportViewModel PackageImportViewModel =>
DialogFactory.Get<PackageImportViewModel>();
public static RefreshBadgeViewModel RefreshBadgeViewModel => new()
{
State = ProgressState.Success
};
public static RefreshBadgeViewModel RefreshBadgeViewModel =>
new() { State = ProgressState.Success };
}

15
StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs

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

4
StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs

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

122
StabilityMatrix.Avalonia/Program.cs

@ -31,9 +31,9 @@ namespace StabilityMatrix.Avalonia;
public class Program
{
public static AppArgs Args { get; } = new();
public static bool IsDebugBuild { get; private set; }
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
@ -47,28 +47,30 @@ public class Program
Args.ResetWindowPosition = args.Contains("--reset-window-position");
SetDebugBuild();
HandleUpdateReplacement();
var infoVersion = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
var infoVersion = Assembly
.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
Compat.AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict);
// Configure exception dialog for unhandled exceptions
if (!Debugger.IsAttached || Args.DebugExceptionDialog)
{
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
}
// Configure Sentry
if (!Args.NoSentry && (!Debugger.IsAttached || Args.DebugSentry))
{
ConfigureSentry();
}
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
@ -76,24 +78,23 @@ public class Program
// Use our custom image loader for custom local load error handling
ImageLoader.AsyncImageLoader.Dispose();
ImageLoader.AsyncImageLoader = new FallbackRamCachedWebImageLoader();
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
return AppBuilder.Configure<App>().UsePlatformDetect().WithInterFont().LogToTrace();
}
private static void HandleUpdateReplacement()
{
// Check if we're in the named update folder or the legacy update folder for 1.2.0 -> 2.0.0
if (Compat.AppCurrentDir is {Name: UpdateHelper.UpdateFolderName} or {Name: "Update"})
if (Compat.AppCurrentDir is { Name: UpdateHelper.UpdateFolderName } or { Name: "Update" })
{
var parentDir = Compat.AppCurrentDir.Parent;
if (parentDir is null)
if (parentDir is null)
return;
var retryDelays = Backoff.DecorrelatedJitterBackoffV2(
TimeSpan.FromMilliseconds(350), retryCount: 5);
TimeSpan.FromMilliseconds(350),
retryCount: 5
);
foreach (var delay in retryDelays)
{
@ -103,20 +104,25 @@ public class Program
try
{
currentExe.CopyTo(targetExe, true);
// Ensure permissions are set for unix
if (Compat.IsUnix)
{
File.SetUnixFileMode(targetExe, // 0755
UnixFileMode.UserRead | UnixFileMode.UserWrite |
UnixFileMode.UserExecute | UnixFileMode.GroupRead |
UnixFileMode.GroupExecute | UnixFileMode.OtherRead |
UnixFileMode.OtherExecute);
File.SetUnixFileMode(
targetExe, // 0755
UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
| UnixFileMode.GroupRead
| UnixFileMode.GroupExecute
| UnixFileMode.OtherRead
| UnixFileMode.OtherExecute
);
}
// Start the new app
Process.Start(targetExe);
// Shutdown the current app
Environment.Exit(0);
}
@ -126,7 +132,7 @@ public class Program
}
}
}
// Delete update folder if it exists in current directory
var updateDir = UpdateHelper.UpdateFolder;
if (updateDir.Exists)
@ -142,12 +148,13 @@ public class Program
}
}
}
private static void ConfigureSentry()
{
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.TracesSampleRate = 1.0;
o.IsGlobalModeEnabled = true;
@ -160,47 +167,56 @@ public class Program
#endif
});
}
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();
logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message);
if (SentrySdk.IsEnabled)
{
SentrySdk.CaptureException(ex);
}
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
if (
Application.Current?.ApplicationLifetime
is IClassicDesktopStyleApplicationLifetime lifetime
)
{
var dialog = new ExceptionDialog
{
DataContext = new ExceptionViewModel
{
Exception = ex
}
DataContext = new ExceptionViewModel { Exception = ex }
};
var mainWindow = lifetime.MainWindow;
// We can only show dialog if main window exists, and is visible
if (mainWindow is {PlatformImpl: not null, IsVisible: true})
if (mainWindow is { PlatformImpl: not null, IsVisible: true })
{
// Configure for dialog mode
dialog.ShowAsDialog = true;
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
// Show synchronously without blocking UI thread
// https://github.com/AvaloniaUI/Avalonia/issues/4810#issuecomment-704259221
var cts = new CancellationTokenSource();
dialog.ShowDialog(mainWindow).ContinueWith(_ =>
{
cts.Cancel();
ExitWithException(ex);
}, TaskScheduler.FromCurrentSynchronizationContext());
dialog
.ShowDialog(mainWindow)
.ContinueWith(
_ =>
{
cts.Cancel();
ExitWithException(ex);
},
TaskScheduler.FromCurrentSynchronizationContext()
);
Dispatcher.UIThread.MainLoop(cts.Token);
}
else
@ -209,9 +225,9 @@ public class Program
var cts = new CancellationTokenSource();
// Exit on token cancellation
cts.Token.Register(() => ExitWithException(ex));
dialog.ShowWithCts(cts);
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 FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public class ContentDialogViewModelBase : ViewModelBase
{

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

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

199
StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs

@ -20,18 +20,21 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private bool isDisposed;
// Queue for console updates
private BufferBlock<ProcessOutput> buffer = new();
// Task that updates the console (runs on UI thread)
private Task? updateTask;
// Cancellation token source for updateTask
private CancellationTokenSource? updateCts;
public bool IsUpdatesRunning => updateTask?.IsCompleted == false;
[ObservableProperty] private TextDocument document = new();
[ObservableProperty]
private TextDocument document = new();
/// <summary>
/// Current offset for write operations.
/// </summary>
@ -47,13 +50,13 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
/// </summary>
// ReSharper disable once MemberCanBePrivate.Global
public TimeSpan WriteCursorLockTimeout { get; init; } = TimeSpan.FromMilliseconds(100);
/// <summary>
/// Gets a cancellation token using the cursor lock timeout
/// </summary>
private CancellationToken WriteCursorLockTimeoutToken =>
private CancellationToken WriteCursorLockTimeoutToken =>
new CancellationTokenSource(WriteCursorLockTimeout).Token;
/// <summary>
/// Event invoked when an ApcMessage of type Input is received.
/// </summary>
@ -72,7 +75,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
updateCts = new CancellationTokenSource();
updateTask = Dispatcher.UIThread.InvokeAsync(ConsoleUpdateLoop, DispatcherPriority.Send);
}
/// <summary>
/// Cancels the update task and waits for it to complete.
/// </summary>
@ -97,7 +100,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
// Cancel update task
updateCts?.Cancel();
updateCts = null;
// Wait for update task
if (updateTask is not null)
{
@ -106,7 +109,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
}
Logger.Trace($"Stopped console updates with {buffer.Count} buffer items remaining");
}
/// <summary>
/// Clears the console and sets a new buffer.
/// This also resets the write cursor to 0.
@ -121,7 +124,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
buffer.Complete();
buffer = new BufferBlock<ProcessOutput>();
}
/// <summary>
/// Resets the write cursor to be equal to the document length.
/// </summary>
@ -129,20 +132,23 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{
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;
}
DebugPrintDocument();
}
private async Task ConsoleUpdateLoop()
{
// This must be run in the UI thread
Dispatcher.UIThread.VerifyAccess();
// Get cancellation token
var ct = updateCts?.Token
?? throw new InvalidOperationException("Update cancellation token must be set");
var ct =
updateCts?.Token
?? throw new InvalidOperationException("Update cancellation token must be set");
try
{
@ -161,16 +167,19 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
}
var outputType = output.IsStdErr ? "stderr" : "stdout";
Debug.WriteLine($"Processing: [{outputType}] (Text = {output.Text.ToRepr()}, " +
$"Raw = {output.RawText?.ToRepr()}, " +
$"CarriageReturn = {output.CarriageReturn}, " +
$"CursorUp = {output.CursorUp}, " +
$"AnsiCommand = {output.AnsiCommand})");
Logger.ConditionalTrace(
$"Processing: [{outputType}] (Text = {output.Text.ToRepr()}, "
+ $"Raw = {output.RawText?.ToRepr()}, "
+ $"CarriageReturn = {output.CarriageReturn}, "
+ $"CursorUp = {output.CursorUp}, "
+ $"AnsiCommand = {output.AnsiCommand})"
);
// Link the cancellation token to the write cursor lock timeout
var linkedCt = CancellationTokenSource
.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken).Token;
.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken)
.Token;
using (await writeCursorLock.LockAsync(linkedCt))
{
ConsoleUpdateOne(output);
@ -184,7 +193,10 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
catch (Exception e)
{
// 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}"
);
}
}
@ -196,7 +208,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
private void ConsoleUpdateOne(ProcessOutput output)
{
Debug.Assert(Dispatcher.UIThread.CheckAccess());
// Check for Apc messages
if (output.ApcMessage is not null)
{
@ -209,21 +221,23 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
// Ignore further processing
return;
}
// If we have a carriage return,
// start current write at the beginning of the current line
if (output.CarriageReturn > 0)
{
var currentLine = Document.GetLineByOffset(writeCursor);
// Get the start of current line as new write cursor
var lineStartOffset = currentLine.Offset;
// See if we need to move the cursor
if (lineStartOffset == writeCursor)
{
Debug.WriteLine($"Cursor already at start for carriage return " +
$"(offset = {lineStartOffset}, line = {currentLine.LineNumber})");
Logger.ConditionalTrace(
$"Cursor already at start for carriage return "
+ $"(offset = {lineStartOffset}, line = {currentLine.LineNumber})"
);
}
else
{
@ -232,19 +246,21 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
var lineEndOffset = currentLine.EndOffset;
var lineLength = lineEndOffset - lineStartOffset;
Document.Remove(lineStartOffset, lineLength);
Debug.WriteLine($"Moving cursor to start for carriage return " +
$"({writeCursor} -> {lineStartOffset})");
Logger.ConditionalTrace(
$"Moving cursor to start for carriage return "
+ $"({writeCursor} -> {lineStartOffset})"
);
writeCursor = lineStartOffset;
}
}
// Write new text
if (!string.IsNullOrEmpty(output.Text))
{
DirectWriteLinesToConsole(output.Text);
}
// Handle cursor movements
if (output.CursorUp > 0)
{
@ -254,22 +270,27 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (currentLocation.Line == 1)
{
// 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
{
// 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);
// Update cursor to target offset
Debug.WriteLine($"Cursor up: Moving (line {currentLocation.Line}, {writeCursor})" +
$" -> (line {targetLocation.Line}, {targetOffset})");
Logger.ConditionalTrace(
$"Cursor up: Moving (line {currentLocation.Line}, {writeCursor})"
+ $" -> (line {targetLocation.Line}, {targetOffset})"
);
writeCursor = targetOffset;
}
}
// Handle erase commands, different to cursor move as they don't move the cursor
// We'll insert blank spaces instead
if (output.AnsiCommand.HasFlag(AnsiCommand.EraseLine))
@ -278,9 +299,11 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
var currentLine = Document.GetLineByOffset(writeCursor);
// Make some spaces to insert
var spaces = new string(' ', currentLine.Length);
// Insert the text
Debug.WriteLine($"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})");
Logger.ConditionalTrace(
$"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})"
);
using (Document.RunUpdate())
{
Document.Replace(currentLine.Offset, currentLine.Length, spaces);
@ -289,7 +312,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
DebugPrintDocument();
}
/// <summary>
/// Write text potentially containing line breaks to the console.
/// <remarks>This call will hold a upgradeable read lock</remarks>
@ -298,7 +321,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{
// When our cursor is not at end, newlines should be interpreted as commands to
// move cursor forward to the next linebreak instead of inserting a newline.
// If text contains no newlines, we can just call DirectWriteToConsole
// Also if cursor is equal to document length
if (!text.Contains(Environment.NewLine) || writeCursor == Document.TextLength)
@ -306,7 +329,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
DirectWriteToConsole(text);
return;
}
// Otherwise we need to handle how linebreaks are treated
// Split text into lines
var lines = text.Split(Environment.NewLine).ToList();
@ -315,29 +338,32 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{
// Insert text
DirectWriteToConsole(lineText);
// Set cursor to start of next line, if we're not already there
var currentLine = Document.GetLineByOffset(writeCursor);
// If next line is available, move cursor to start of next line
if (currentLine.LineNumber < Document.LineCount)
{
var nextLine = Document.GetLineByNumber(currentLine.LineNumber + 1);
Debug.WriteLine($"Moving cursor to start of next line " +
$"({writeCursor} -> {nextLine.Offset})");
Logger.ConditionalTrace(
$"Moving cursor to start of next line "
+ $"({writeCursor} -> {nextLine.Offset})"
);
writeCursor = nextLine.Offset;
}
else
{
// Otherwise move to end of current line, and direct insert a newline
var lineEndOffset = currentLine.EndOffset;
Debug.WriteLine($"Moving cursor to end of current line " +
$"({writeCursor} -> {lineEndOffset})");
Logger.ConditionalTrace(
$"Moving cursor to end of current line " + $"({writeCursor} -> {lineEndOffset})"
);
writeCursor = lineEndOffset;
DirectWriteToConsole(Environment.NewLine);
}
}
}
/// <summary>
/// Write text to the console, does not handle newlines.
/// This should probably only be used by <see cref="DirectWriteLinesToConsole"/>
@ -352,10 +378,11 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (replaceLength > 0)
{
var newText = text[..replaceLength];
Debug.WriteLine(
$"Replacing: (cursor = {writeCursor}, length = {replaceLength}, " +
$"text = {Document.GetText(writeCursor, replaceLength).ToRepr()} " +
$"-> {newText.ToRepr()})");
Logger.ConditionalTrace(
$"Replacing: (cursor = {writeCursor}, length = {replaceLength}, "
+ $"text = {Document.GetText(writeCursor, replaceLength).ToRepr()} "
+ $"-> {newText.ToRepr()})"
);
Document.Replace(writeCursor, replaceLength, newText);
writeCursor += replaceLength;
@ -366,8 +393,9 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (remainingLength > 0)
{
var textToInsert = text[replaceLength..];
Debug.WriteLine($"Inserting: (cursor = {writeCursor}, " +
$"text = {textToInsert.ToRepr()})");
Logger.ConditionalTrace(
$"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})"
);
Document.Insert(writeCursor, textToInsert);
writeCursor += textToInsert.Length;
@ -382,6 +410,9 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
[Conditional("DEBUG")]
private void DebugPrintDocument()
{
if (!Logger.IsTraceEnabled)
return;
var text = Document.Text;
// Add a number for each line
// Add an arrow line for where the cursor is, for example (cursor on offset 3):
@ -390,7 +421,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
// ~~~~~~~^ (3)
// 2 | This is the second line
//
var lines = text.Split(Environment.NewLine).ToList();
var numberPadding = lines.Count.ToString().Length;
for (var i = 0; i < lines.Count; i++)
@ -399,11 +430,11 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
}
var cursorLine = Document.GetLineByOffset(writeCursor);
var cursorLineOffset = writeCursor - cursorLine.Offset;
// Need to account for padding + line number + space + cursor line offset
var linePadding = numberPadding + 3 + cursorLineOffset;
var cursorLineArrow = new string('~', linePadding) + $"^ ({writeCursor})";
// If more than line count, append to end
if (cursorLine.LineNumber >= lines.Count)
{
@ -413,13 +444,13 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{
lines.Insert(cursorLine.LineNumber, cursorLineArrow);
}
var textWithCursor = string.Join(Environment.NewLine, lines);
Debug.WriteLine("[Current Document]");
Debug.WriteLine(textWithCursor);
Logger.ConditionalTrace("[Current Document]");
Logger.ConditionalTrace(textWithCursor);
}
/// <summary>
/// Posts an update to the console
/// <remarks>Safe to call on non-UI threads</remarks>
@ -433,9 +464,10 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
return;
}
// Otherwise, use manual update one
Logger.Debug("Synchronous post update to console: {@Output}", output);
Dispatcher.UIThread.Post(() => ConsoleUpdateOne(output));
}
/// <summary>
/// Posts an update to the console.
/// Helper for calling Post(ProcessOutput) with strings
@ -444,7 +476,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{
Post(new ProcessOutput { Text = text });
}
/// <summary>
/// Posts an update to the console.
/// Equivalent to Post(text + Environment.NewLine)
@ -456,20 +488,22 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
public void Dispose()
{
if (isDisposed) return;
if (isDisposed)
return;
updateCts?.Cancel();
updateCts?.Dispose();
updateCts = null;
buffer.Complete();
if (updateTask is not null)
{
Logger.Debug("Shutting down console update task");
try
{
updateTask.WaitWithoutException(
new CancellationTokenSource(1000).Token);
updateTask.WaitWithoutException(new CancellationTokenSource(1000).Token);
updateTask.Dispose();
updateTask = null;
}
@ -484,27 +518,32 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
}
isDisposed = true;
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync()
{
if (isDisposed) return;
if (isDisposed)
return;
updateCts?.Cancel();
updateCts?.Dispose();
updateCts = null;
if (updateTask is not null)
{
Logger.Debug("Waiting for console update task shutdown...");
await updateTask;
updateTask.Dispose();
updateTask = null;
Logger.Debug("Console update task shutdown complete");
}
isDisposed = true;
GC.SuppressFinalize(this);
}
}

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

@ -4,6 +4,7 @@ using System.Diagnostics;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
@ -19,7 +20,7 @@ public partial class EnvVarsViewModel : ContentDialogViewModelBase
private ObservableCollection<EnvVarKeyPair> envVars = new();
public DataGridCollectionView EnvVarsView => new(EnvVars);
[RelayCommand]
private void AddRow()
{

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

@ -17,10 +17,12 @@ using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
@ -29,59 +31,98 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
public partial class InstallerViewModel : ContentDialogViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
private readonly IPyRunner pyRunner;
private readonly IDownloadService downloadService;
private readonly INotificationService notificationService;
private readonly IPrerequisiteHelper prerequisiteHelper;
[ObservableProperty] 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 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 string latestVersionText = string.Empty;
[ObservableProperty]
private bool isAdvancedMode;
[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)
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ReleaseLabelText),
nameof(IsReleaseMode), nameof(SelectedVersion))]
[NotifyPropertyChangedFor(
nameof(ReleaseLabelText),
nameof(IsReleaseMode),
nameof(SelectedVersion)
)]
private PackageVersionType selectedVersionType = PackageVersionType.Commit;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))]
private PackageVersionType availableVersionTypes =
private PackageVersionType availableVersionTypes =
PackageVersionType.GithubRelease | PackageVersionType.Commit;
public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch";
public bool IsReleaseMode
{
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 CanInstall => !string.IsNullOrWhiteSpace(InstallName) && !ShowDuplicateWarning;
public bool IsReleaseModeAvailable => AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease);
[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(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
IPyRunner pyRunner,
IDownloadService downloadService, INotificationService notificationService,
IPrerequisiteHelper prerequisiteHelper)
IDownloadService downloadService,
INotificationService notificationService,
IPrerequisiteHelper prerequisiteHelper
)
{
this.settingsManager = settingsManager;
this.pyRunner = pyRunner;
@ -90,40 +131,43 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
this.prerequisiteHelper = prerequisiteHelper;
// AvailablePackages and SelectedPackage
AvailablePackages = new ObservableCollection<BasePackage>(packageFactory.GetAllAvailablePackages());
AvailablePackages = new ObservableCollection<BasePackage>(
packageFactory.GetAllAvailablePackages()
);
SelectedPackage = AvailablePackages[0];
}
public override void OnLoaded()
{
if (AvailablePackages == null) return;
if (AvailablePackages == null)
return;
SelectedPackage = AvailablePackages[0];
IsReleaseMode = !SelectedPackage.ShouldIgnoreReleases;
}
public override async Task OnLoadedAsync()
{
if (Design.IsDesignMode) return;
if (Design.IsDesignMode)
return;
// Check for updates
try
{
var versionOptions = await SelectedPackage.GetAllVersionOptions();
if (IsReleaseMode)
{
var versions = (await SelectedPackage.GetAllVersions()).ToList();
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
if (!AvailableVersions.Any()) return;
AvailableVersions = new ObservableCollection<PackageVersion>(
versionOptions.AvailableVersions
);
if (!AvailableVersions.Any())
return;
SelectedVersion = AvailableVersions[0];
SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease);
}
else
{
var branches = (await SelectedPackage.GetAllBranches()).ToList();
AvailableVersions = new ObservableCollection<PackageVersion>(branches.Select(b =>
new PackageVersion
{
TagName = b.Name,
ReleaseNotesMarkdown = b.Commit.Label
}));
AvailableVersions = new ObservableCollection<PackageVersion>(
versionOptions.AvailableBranches
);
UpdateSelectedVersionToLatestMain();
}
@ -134,17 +178,17 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
Logger.Warn("Error getting versions: {Exception}", e.ToString());
}
}
[RelayCommand]
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)
{
notificationService.Show(new Notification(
$"Package {SelectedPackage.Name} installed successfully!",
"Success", NotificationType.Success));
OnPrimaryButtonClick();
OnPrimaryButtonClick();
}
else
{
@ -160,92 +204,95 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
await dialog.ShowAsync();
}
}
private async Task ActuallyInstall()
private Task ActuallyInstall()
{
if (string.IsNullOrWhiteSpace(InstallName))
{
notificationService.Show(new Notification("Package name is empty",
"Please enter a name for the package", NotificationType.Error));
return;
notificationService.Show(
new Notification(
"Package name is empty",
"Please enter a name for the package",
NotificationType.Error
)
);
return Task.CompletedTask;
}
try
{
await InstallGitIfNecessary();
SelectedPackage.InstallLocation = Path.Combine(
settingsManager.LibraryDir, "Packages", InstallName);
if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled)
{
InstallProgress.Text = "Installing dependencies...";
InstallProgress.IsIndeterminate = true;
await pyRunner.Initialize();
if (!PyRunner.PipInstalled)
{
await pyRunner.SetupPip();
}
if (!PyRunner.VenvInstalled)
{
await pyRunner.InstallPackage("virtualenv");
}
}
var setPackageInstallingStep = new SetPackageInstallingStep(settingsManager, InstallName);
string version;
if (IsReleaseMode)
{
version = SelectedVersion?.TagName ??
throw new NullReferenceException("Selected version is null");
await DownloadPackage(version, false, null);
}
else
{
version = SelectedCommit?.Sha ??
throw new NullReferenceException("Selected commit is null");
await DownloadPackage(version, true, SelectedVersion!.TagName);
}
await InstallPackage();
InstallProgress.Text = "Setting up shared folder links...";
await SelectedPackage.SetupModelFolders(SelectedPackage.InstallLocation);
//sharedFolders.SetupLinksForPackage(SelectedPackage, SelectedPackage.InstallLocation);
InstallProgress.Text = "Done";
InstallProgress.IsIndeterminate = false;
InstallProgress.Value = 100;
EventManager.Instance.OnGlobalProgressChanged(100);
var branch = SelectedVersionType == PackageVersionType.GithubRelease ?
null : SelectedVersion!.TagName;
var package = new InstalledPackage
{
DisplayName = InstallName,
LibraryPath = Path.Combine("Packages", InstallName),
Id = Guid.NewGuid(),
PackageName = SelectedPackage.Name,
PackageVersion = version,
DisplayVersion = GetDisplayVersion(version, branch),
InstalledBranch = branch,
LaunchCommand = SelectedPackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now
};
await using var st = settingsManager.BeginTransaction();
st.Settings.InstalledPackages.Add(package);
st.Settings.ActiveInstalledPackageId = package.Id;
var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", InstallName);
var prereqStep = new SetupPrerequisitesStep(prerequisiteHelper, pyRunner);
var downloadOptions = new DownloadPackageVersionOptions();
var installedVersion = new InstalledPackageVersion();
if (IsReleaseMode)
{
downloadOptions.VersionTag =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag;
}
finally
else
{
InstallProgress.Value = 0;
InstallProgress.IsIndeterminate = false;
downloadOptions.CommitHash =
SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null");
installedVersion.InstalledBranch =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
installedVersion.InstalledCommitSha = downloadOptions.CommitHash;
}
var downloadStep = new DownloadPackageVersionStep(
SelectedPackage,
installLocation,
downloadOptions
);
var installStep = new InstallPackageStep(
SelectedPackage,
SelectedTorchVersion,
installLocation
);
var setupModelFoldersStep = new SetupModelFoldersStep(
SelectedPackage,
SelectedSharedFolderMethod,
installLocation
);
var package = new InstalledPackage
{
DisplayName = InstallName,
LibraryPath = Path.Combine("Packages", InstallName),
Id = Guid.NewGuid(),
PackageName = SelectedPackage.Name,
Version = installedVersion,
LaunchCommand = SelectedPackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now,
PreferredTorchVersion = SelectedTorchVersion,
PreferredSharedFolderMethod = SelectedSharedFolderMethod
};
var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, package);
var steps = new List<IPackageStep>
{
setPackageInstallingStep,
prereqStep,
downloadStep,
installStep,
setupModelFoldersStep,
addInstalledPackageStep
};
Steps = steps;
return Task.CompletedTask;
}
public void Cancel()
{
OnCloseButtonClick();
}
private void UpdateSelectedVersionToLatestMain()
{
if (AvailableVersions is null)
@ -258,79 +305,34 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
var version = AvailableVersions.FirstOrDefault(x => x.TagName == "master");
// If not found, try main
version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main");
// If still not found, just use the first one
version ??= AvailableVersions[0];
version ??= AvailableVersions.FirstOrDefault();
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]
private async Task ShowPreview()
{
var url = SelectedPackage.PreviewImageUri.ToString();
var imageStream = await downloadService.GetImageStreamFromUrl(url);
var bitmap = new Bitmap(imageStream);
var dialog = new ContentDialog
{
PrimaryButtonText = "Open in Browser",
CloseButtonText = "Close",
Content = new Image
{
Source = bitmap,
Stretch = Stretch.Uniform,
Source = bitmap,
Stretch = Stretch.Uniform,
MaxHeight = 500,
HorizontalAlignment = HorizontalAlignment.Center
}
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
@ -346,10 +348,11 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
SelectedVersionType = value;
}
}
// When changing branch / release modes, refresh
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnSelectedVersionTypeChanged(PackageVersionType value) => OnSelectedPackageChanged(SelectedPackage);
partial void OnSelectedVersionTypeChanged(PackageVersionType value) =>
OnSelectedPackageChanged(SelectedPackage);
partial void OnSelectedPackageChanged(BasePackage value)
{
@ -360,99 +363,85 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
AvailableVersionTypes = SelectedPackage.ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
if (Design.IsDesignMode) return;
Dispatcher.UIThread.InvokeAsync(async () =>
{
Logger.Debug($"Release mode: {IsReleaseMode}");
var versions = (await value.GetAllVersions(IsReleaseMode)).ToList();
if (!versions.Any()) return;
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}");
if (!IsReleaseMode)
{
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
if (commits is null || commits.Count == 0) return;
AvailableCommits = new ObservableCollection<GitCommit>(commits);
SelectedCommit = AvailableCommits[0];
UpdateSelectedVersionToLatestMain();
}
InstallName = SelectedPackage.DisplayName;
}).SafeFireAndForget();
}
private async Task InstallGitIfNecessary()
{
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
SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion();
if (Design.IsDesignMode)
return;
Dispatcher.UIThread
.InvokeAsync(async () =>
{
InstallProgress.Text = progress.Message;
}
Logger.Debug($"Release mode: {IsReleaseMode}");
var versionOptions = await value.GetAllVersionOptions();
AvailableVersions = IsReleaseMode
? new ObservableCollection<PackageVersion>(versionOptions.AvailableVersions)
: new ObservableCollection<PackageVersion>(versionOptions.AvailableBranches);
SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease);
ReleaseNotes = SelectedVersion.ReleaseNotesMarkdown;
InstallProgress.IsIndeterminate = progress.IsIndeterminate;
InstallProgress.Value = Convert.ToInt32(progress.Percentage);
});
Logger.Debug($"Loaded release notes for {ReleaseNotes}");
await prerequisiteHelper.InstallAllIfNecessary(progressHandler);
if (!IsReleaseMode)
{
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
if (commits is null || commits.Count == 0)
return;
AvailableCommits = new ObservableCollection<GitCommit>(commits);
SelectedCommit = AvailableCommits.FirstOrDefault();
UpdateSelectedVersionToLatestMain();
}
InstallName = SelectedPackage.DisplayName;
LatestVersionText = IsReleaseMode
? $"Latest version: {SelectedVersion?.TagName}"
: $"Branch: {SelectedVersion?.TagName}";
})
.SafeFireAndForget();
}
partial void OnInstallNameChanged(string? value)
{
ShowDuplicateWarning =
settingsManager.Settings.InstalledPackages.Any(p =>
p.LibraryPath == $"Packages{Path.DirectorySeparatorChar}{value}");
ShowDuplicateWarning = settingsManager.Settings.InstalledPackages.Any(
p => p.LibraryPath == $"Packages{Path.DirectorySeparatorChar}{value}"
);
}
partial void OnSelectedVersionChanged(PackageVersion? value)
{
ReleaseNotes = value?.ReleaseNotesMarkdown ?? string.Empty;
if (value == null) return;
if (value == null)
return;
SelectedCommit = null;
AvailableCommits?.Clear();
if (!IsReleaseMode)
{
Task.Run(async () =>
{
try
{
var hashes = await SelectedPackage.GetAllCommits(value.TagName);
if (hashes is null) throw new Exception("No commits found");
Dispatcher.UIThread.Post(() =>
try
{
AvailableCommits = new ObservableCollection<GitCommit>(hashes);
SelectedCommit = AvailableCommits[0];
});
}
catch (Exception e)
{
Logger.Warn($"Error getting commits: {e.Message}");
}
}).SafeFireAndForget();
var hashes = await SelectedPackage.GetAllCommits(value.TagName);
if (hashes is null)
throw new Exception("No commits found");
Dispatcher.UIThread.Post(() =>
{
AvailableCommits = new ObservableCollection<GitCommit>(hashes);
SelectedCommit = AvailableCommits[0];
});
}
catch (Exception e)
{
Logger.Warn($"Error getting commits: {e.Message}");
}
})
.SafeFireAndForget();
}
}
}

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

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

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

@ -26,14 +26,27 @@ public partial class OneClickInstallViewModel : ViewModelBase
private readonly IPyRunner pyRunner;
private readonly ISharedFolders sharedFolders;
private const string DefaultPackageName = "stable-diffusion-webui";
[ObservableProperty] private string headerText;
[ObservableProperty] private string subHeaderText;
[ObservableProperty] private string subSubHeaderText = string.Empty;
[ObservableProperty] private bool showInstallButton;
[ObservableProperty] private bool isIndeterminate;
[ObservableProperty] private ObservableCollection<BasePackage> allPackages;
[ObservableProperty] private BasePackage selectedPackage;
[ObservableProperty]
private string headerText;
[ObservableProperty]
private string subHeaderText;
[ObservableProperty]
private string subSubHeaderText = string.Empty;
[ObservableProperty]
private bool showInstallButton;
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
private ObservableCollection<BasePackage> allPackages;
[ObservableProperty]
private BasePackage selectedPackage;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsProgressBarVisible))]
@ -41,9 +54,14 @@ public partial class OneClickInstallViewModel : ViewModelBase
public bool IsProgressBarVisible => OneClickInstallProgress > 0 || IsIndeterminate;
public OneClickInstallViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory,
IPrerequisiteHelper prerequisiteHelper, ILogger<OneClickInstallViewModel> logger, IPyRunner pyRunner,
ISharedFolders sharedFolders)
public OneClickInstallViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
IPrerequisiteHelper prerequisiteHelper,
ILogger<OneClickInstallViewModel> logger,
IPyRunner pyRunner,
ISharedFolders sharedFolders
)
{
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
@ -55,9 +73,9 @@ public partial class OneClickInstallViewModel : ViewModelBase
HeaderText = "Welcome to Stability Matrix!";
SubHeaderText = "Choose your preferred interface and click Install to get started!";
ShowInstallButton = true;
AllPackages =
new ObservableCollection<BasePackage>(this.packageFactory.GetAllAvailablePackages()
.Where(p => p.OfferInOneClickInstaller));
AllPackages = new ObservableCollection<BasePackage>(
this.packageFactory.GetAllAvailablePackages().Where(p => p.OfferInOneClickInstaller)
);
SelectedPackage = AllPackages[0];
}
@ -75,7 +93,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
EventManager.Instance.OnOneClickInstallFinished(true);
return Task.CompletedTask;
}
private async Task DoInstall()
{
HeaderText = $"Installing {SelectedPackage.DisplayName}";
@ -83,11 +101,11 @@ public partial class OneClickInstallViewModel : ViewModelBase
var progressHandler = new Progress<ProgressReport>(progress =>
{
SubHeaderText = $"{progress.Title} {progress.Percentage:N0}%";
IsIndeterminate = progress.IsIndeterminate;
OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
});
await prerequisiteHelper.InstallAllIfNecessary(progressHandler);
SubHeaderText = "Installing prerequisites...";
@ -104,36 +122,52 @@ public partial class OneClickInstallViewModel : ViewModelBase
IsIndeterminate = false;
var libraryDir = settingsManager.LibraryDir;
// get latest version & download & install
SubHeaderText = "Getting latest version...";
var latestVersion = await SelectedPackage.GetLatestVersion();
SelectedPackage.InstallLocation =
Path.Combine(libraryDir, "Packages", SelectedPackage.Name);
SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text;
await DownloadPackage(latestVersion);
await InstallPackage();
var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name);
var downloadVersion = new DownloadPackageVersionOptions();
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(installLocation, downloadVersion);
await InstallPackage(installLocation, torchVersion);
SubHeaderText = "Setting up shared folder links...";
await SelectedPackage.SetupModelFolders(SelectedPackage.InstallLocation);
var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
await SelectedPackage.SetupModelFolders(installLocation, recommendedSharedFolderMethod);
var installedPackage = new InstalledPackage
{
DisplayName = SelectedPackage.DisplayName,
LibraryPath = Path.Combine("Packages", SelectedPackage.Name),
Id = Guid.NewGuid(),
PackageName = SelectedPackage.Name,
PackageVersion = latestVersion,
DisplayVersion = latestVersion,
Version = installedVersion,
LaunchCommand = SelectedPackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now
LastUpdateCheck = DateTimeOffset.Now,
PreferredTorchVersion = torchVersion,
PreferredSharedFolderMethod = recommendedSharedFolderMethod
};
await using var st = settingsManager.BeginTransaction();
st.Settings.InstalledPackages.Add(installedPackage);
st.Settings.ActiveInstalledPackageId = installedPackage.Id;
EventManager.Instance.OnInstalledPackagesChanged();
HeaderText = "Installation complete!";
SubSubHeaderText = string.Empty;
OneClickInstallProgress = 100;
@ -143,32 +177,34 @@ public partial class OneClickInstallViewModel : ViewModelBase
await Task.Delay(1000);
SubHeaderText = "Proceeding to Launch page in 1 second...";
await Task.Delay(1000);
// should close dialog
EventManager.Instance.OnOneClickInstallFinished(false);
}
private async Task DownloadPackage(string version)
private async Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions
)
{
SubHeaderText = "Downloading package...";
var progress = new Progress<ProgressReport>(progress =>
{
IsIndeterminate = progress.IsIndeterminate;
OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
});
await SelectedPackage.DownloadPackage(version, false, version, progress);
await SelectedPackage.DownloadPackage(installLocation, versionOptions, progress);
SubHeaderText = "Download Complete";
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...";
var progress = new Progress<ProgressReport>(progress =>
{
SubHeaderText = "Downloading and installing package requirements...";
@ -176,7 +212,12 @@ public partial class OneClickInstallViewModel : ViewModelBase
OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
});
await SelectedPackage.InstallPackage(progress);
await SelectedPackage.InstallPackage(
installLocation,
torchVersion,
progress,
(output) => SubSubHeaderText = output.Text
);
}
}

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

@ -10,6 +10,7 @@ using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Factory;
@ -25,45 +26,58 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
public partial class PackageImportViewModel : ContentDialogViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IPackageFactory packageFactory;
private readonly ISettingsManager settingsManager;
[ObservableProperty] private DirectoryPath? packagePath;
[ObservableProperty] private BasePackage? selectedBasePackage;
public IReadOnlyList<BasePackage> AvailablePackages
=> packageFactory.GetAllAvailablePackages().ToImmutableArray();
[ObservableProperty] private PackageVersion? selectedVersion;
[ObservableProperty] private ObservableCollection<GitCommit>? availableCommits;
[ObservableProperty] private ObservableCollection<PackageVersion>? availableVersions;
[ObservableProperty] private GitCommit? selectedCommit;
[ObservableProperty]
private DirectoryPath? packagePath;
[ObservableProperty]
private BasePackage? selectedBasePackage;
public IReadOnlyList<BasePackage> AvailablePackages =>
packageFactory.GetAllAvailablePackages().ToImmutableArray();
[ObservableProperty]
private PackageVersion? selectedVersion;
[ObservableProperty]
private ObservableCollection<GitCommit>? availableCommits;
[ObservableProperty]
private ObservableCollection<PackageVersion>? availableVersions;
[ObservableProperty]
private GitCommit? selectedCommit;
// Version types (release or commit)
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ReleaseLabelText),
nameof(IsReleaseMode), nameof(SelectedVersion))]
[NotifyPropertyChangedFor(
nameof(ReleaseLabelText),
nameof(IsReleaseMode),
nameof(SelectedVersion)
)]
private PackageVersionType selectedVersionType = PackageVersionType.Commit;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))]
private PackageVersionType availableVersionTypes =
private PackageVersionType availableVersionTypes =
PackageVersionType.GithubRelease | PackageVersionType.Commit;
public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch";
public bool IsReleaseMode
{
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 PackageImportViewModel(
IPackageFactory packageFactory,
ISettingsManager settingsManager)
public bool IsReleaseModeAvailable =>
AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease);
public PackageImportViewModel(IPackageFactory packageFactory, ISettingsManager settingsManager)
{
this.packageFactory = packageFactory;
this.settingsManager = settingsManager;
@ -72,28 +86,28 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
public override async Task OnLoadedAsync()
{
SelectedBasePackage ??= AvailablePackages[0];
if (Design.IsDesignMode) return;
if (Design.IsDesignMode)
return;
// Populate available versions
try
{
var versionOptions = await SelectedBasePackage.GetAllVersionOptions();
if (IsReleaseMode)
{
var versions = (await SelectedBasePackage.GetAllVersions()).ToList();
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
if (!AvailableVersions.Any()) return;
AvailableVersions = new ObservableCollection<PackageVersion>(
versionOptions.AvailableVersions
);
if (!AvailableVersions.Any())
return;
SelectedVersion = AvailableVersions[0];
}
else
{
var branches = (await SelectedBasePackage.GetAllBranches()).ToList();
AvailableVersions = new ObservableCollection<PackageVersion>(branches.Select(b =>
new PackageVersion
{
TagName = b.Name,
ReleaseNotesMarkdown = b.Commit.Label
}));
AvailableVersions = new ObservableCollection<PackageVersion>(
versionOptions.AvailableBranches
);
UpdateSelectedVersionToLatestMain();
}
}
@ -102,12 +116,12 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
Logger.Warn("Error getting versions: {Exception}", e.ToString());
}
}
private static string GetDisplayVersion(string version, string? branch)
{
return branch == null ? version : $"{branch}@{version[..7]}";
}
// When available version types change, reset selected version type if not compatible
partial void OnAvailableVersionTypesChanged(PackageVersionType value)
{
@ -116,11 +130,11 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
SelectedVersionType = value;
}
}
// When changing branch / release modes, refresh
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnSelectedVersionTypeChanged(PackageVersionType value)
=> OnSelectedBasePackageChanged(SelectedBasePackage);
partial void OnSelectedVersionTypeChanged(PackageVersionType value) =>
OnSelectedBasePackageChanged(SelectedBasePackage);
partial void OnSelectedBasePackageChanged(BasePackage? value)
{
@ -130,39 +144,42 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
AvailableCommits?.Clear();
return;
}
AvailableVersions?.Clear();
AvailableCommits?.Clear();
AvailableVersionTypes = SelectedBasePackage.ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
if (Design.IsDesignMode) return;
Dispatcher.UIThread.InvokeAsync(async () =>
{
Logger.Debug($"Release mode: {IsReleaseMode}");
var versions = (await value.GetAllVersions(IsReleaseMode)).ToList();
if (!versions.Any()) return;
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}");
SelectedVersion = AvailableVersions[0];
if (!IsReleaseMode)
AvailableVersionTypes = SelectedBasePackage.AvailableVersionTypes;
if (Design.IsDesignMode)
return;
Dispatcher.UIThread
.InvokeAsync(async () =>
{
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
if (commits is null || commits.Count == 0) return;
AvailableCommits = new ObservableCollection<GitCommit>(commits);
SelectedCommit = AvailableCommits[0];
UpdateSelectedVersionToLatestMain();
}
}).SafeFireAndForget();
Logger.Debug($"Release mode: {IsReleaseMode}");
var versionOptions = await value.GetAllVersionOptions();
AvailableVersions = IsReleaseModeAvailable
? new ObservableCollection<PackageVersion>(versionOptions.AvailableVersions)
: new ObservableCollection<PackageVersion>(versionOptions.AvailableBranches);
Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}");
SelectedVersion = AvailableVersions[0];
if (!IsReleaseMode)
{
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
if (commits is null || commits.Count == 0)
return;
AvailableCommits = new ObservableCollection<GitCommit>(commits);
SelectedCommit = AvailableCommits[0];
UpdateSelectedVersionToLatestMain();
}
})
.SafeFireAndForget();
}
private void UpdateSelectedVersionToLatestMain()
{
if (AvailableVersions is null)
@ -175,56 +192,60 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
var version = AvailableVersions.FirstOrDefault(x => x.TagName == "master");
// If not found, try main
version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main");
// If still not found, just use the first one
version ??= AvailableVersions[0];
SelectedVersion = version;
}
}
public async Task AddPackageWithCurrentInputs()
{
if (SelectedBasePackage is null || PackagePath is null)
return;
string version;
var version = new InstalledPackageVersion();
if (IsReleaseMode)
{
version = SelectedVersion?.TagName ??
throw new NullReferenceException("Selected version is null");
version.InstalledReleaseVersion =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
}
else
{
version = SelectedCommit?.Sha ??
throw new NullReferenceException("Selected commit is null");
version.InstalledBranch =
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 ?
null : SelectedVersion!.TagName;
var torchVersion = SelectedBasePackage.GetRecommendedTorchVersion();
var sharedFolderRecommendation = SelectedBasePackage.RecommendedSharedFolderMethod;
var package = new InstalledPackage
{
Id = Guid.NewGuid(),
DisplayName = PackagePath.Name,
PackageName = SelectedBasePackage.Name,
LibraryPath = $"Packages{Path.DirectorySeparatorChar}{PackagePath.Name}",
PackageVersion = version,
DisplayVersion = GetDisplayVersion(version, branch),
InstalledBranch = branch,
Version = version,
LaunchCommand = SelectedBasePackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now,
PreferredTorchVersion = torchVersion,
PreferredSharedFolderMethod = sharedFolderRecommendation
};
// Recreate venv if it's a BaseGitPackage
if (SelectedBasePackage is BaseGitPackage gitPackage)
{
await gitPackage.SetupVenv(PackagePath, forceRecreate: true);
}
// Reconfigure shared links
await SelectedBasePackage.UpdateModelFolders(PackagePath);
var recommendedSharedFolderMethod = SelectedBasePackage.RecommendedSharedFolderMethod;
await SelectedBasePackage.UpdateModelFolders(PackagePath, recommendedSharedFolderMethod);
settingsManager.Transaction(s => s.InstalledPackages.Add(package));
}
}

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

@ -9,6 +9,7 @@ using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
@ -23,33 +24,43 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public static string DefaultInstallLocation => Compat.AppDataHome;
private readonly ISettingsManager settingsManager;
private const string ValidExistingDirectoryText = "Valid existing data directory found";
private const string InvalidDirectoryText =
"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 FatWarningText =
"FAT32 / exFAT drives are not supported at this time";
[ObservableProperty] private string dataDirectory = DefaultInstallLocation;
[ObservableProperty] private bool isPortableMode;
[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,
SuccessToolTipText = ValidExistingDirectoryText,
FailToolTipText = InvalidDirectoryText
};
private const string FatWarningText = "FAT32 / exFAT drives are not supported at this time";
[ObservableProperty]
private string dataDirectory = DefaultInstallLocation;
[ObservableProperty]
private bool isPortableMode;
[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,
SuccessToolTipText = ValidExistingDirectoryText,
FailToolTipText = InvalidDirectoryText
};
public bool HasOldData => settingsManager.GetOldInstalledPackages().Any();
public SelectDataDirectoryViewModel(ISettingsManager settingsManager)
{
this.settingsManager = settingsManager;
@ -60,17 +71,17 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
{
ValidatorRefreshBadge.RefreshCommand.ExecuteAsync(null).SafeFireAndForget();
}
// Revalidate on data directory change
partial void OnDataDirectoryChanged(string value)
{
ValidatorRefreshBadge.RefreshCommand.ExecuteAsync(null).SafeFireAndForget();
}
private async Task<bool> ValidateDataDirectory()
{
await using var delay = new MinimumDelay(100, 200);
ShowFatWarning = IsDriveFat(DataDirectory);
// Doesn't exist, this is fine as a new install, hide badge
@ -82,17 +93,17 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
}
// Otherwise check that a settings.json exists
var settingsPath = Path.Combine(DataDirectory, "settings.json");
// settings.json exists: Try deserializing it
if (File.Exists(settingsPath))
{
try
{
var jsonText = await File.ReadAllTextAsync(settingsPath);
JsonSerializer.Deserialize<Settings>(jsonText, new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter() }
});
JsonSerializer.Deserialize<Settings>(
jsonText,
new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }
);
// If successful, show existing badge
IsStatusBadgeVisible = true;
IsDirectoryValid = true;
@ -109,9 +120,9 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
return false;
}
}
// No settings.json
// Check if the directory is %APPDATA%\StabilityMatrix: hide badge and set directory valid
if (DataDirectory == DefaultInstallLocation)
{
@ -119,7 +130,7 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
IsDirectoryValid = true;
return true;
}
// Check if the directory is empty: hide badge and set directory to valid
var isEmpty = !Directory.EnumerateFileSystemEntries(DataDirectory).Any();
if (isEmpty)
@ -135,21 +146,20 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
DirectoryStatusText = InvalidDirectoryText;
return false;
}
private bool CanPickFolder => App.StorageProvider.CanPickFolder;
[RelayCommand(CanExecute = nameof(CanPickFolder))]
private async Task ShowFolderBrowserDialog()
{
var provider = App.StorageProvider;
var result = await provider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Data Folder",
AllowMultiple = false
});
if (result.Count != 1) return;
var result = await provider.OpenFolderPickerAsync(
new FolderPickerOpenOptions { Title = "Select Data Folder", AllowMultiple = false }
);
if (result.Count != 1)
return;
DataDirectory = result[0].Path.LocalPath;
}

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

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

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

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
@ -23,18 +24,29 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
private readonly IHttpClientFactory httpClientFactory;
private readonly IUpdateHelper updateHelper;
[ObservableProperty] private bool isUpdateAvailable;
[ObservableProperty] private UpdateInfo? updateInfo;
[ObservableProperty] private string? releaseNotes;
[ObservableProperty] private string? updateText;
[ObservableProperty] private int progressValue;
[ObservableProperty] private bool showProgressBar;
[ObservableProperty]
private bool isUpdateAvailable;
[ObservableProperty]
private UpdateInfo? updateInfo;
[ObservableProperty]
private string? releaseNotes;
[ObservableProperty]
private string? updateText;
[ObservableProperty]
private int progressValue;
[ObservableProperty]
private bool showProgressBar;
public UpdateViewModel(
ISettingsManager settingsManager,
IHttpClientFactory httpClientFactory,
IUpdateHelper updateHelper)
IHttpClientFactory httpClientFactory,
IUpdateHelper updateHelper
)
{
this.settingsManager = settingsManager;
this.httpClientFactory = httpClientFactory;
@ -47,19 +59,21 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
};
updateHelper.StartCheckingForUpdates().SafeFireAndForget();
}
public override async Task OnLoadedAsync()
{
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?";
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?";
var client = httpClientFactory.CreateClient();
var response = await client.GetAsync(UpdateInfo.ChangelogUrl);
if (response.IsSuccessStatusCode)
{
ReleaseNotes = await response.Content.ReadAsStringAsync();
// Formatting for new changelog format
// https://keepachangelog.com/en/1.1.0/
if (UpdateInfo.ChangelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
@ -86,24 +100,32 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
{
return;
}
ShowProgressBar = true;
UpdateText = $"Downloading update v{UpdateInfo.Version}...";
await updateHelper.DownloadUpdate(UpdateInfo, new Progress<ProgressReport>(report =>
{
ProgressValue = Convert.ToInt32(report.Percentage);
}));
await updateHelper.DownloadUpdate(
UpdateInfo,
new Progress<ProgressReport>(report =>
{
ProgressValue = Convert.ToInt32(report.Percentage);
})
);
// On unix, we need to set the executable bit
if (Compat.IsUnix)
{
File.SetUnixFileMode(UpdateHelper.ExecutablePath, // 0755
UnixFileMode.UserRead | UnixFileMode.UserWrite |
UnixFileMode.UserExecute | UnixFileMode.GroupRead |
UnixFileMode.GroupExecute | UnixFileMode.OtherRead |
UnixFileMode.OtherExecute);
File.SetUnixFileMode(
UpdateHelper.ExecutablePath, // 0755
UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
| UnixFileMode.GroupRead
| UnixFileMode.GroupExecute
| UnixFileMode.OtherRead
| UnixFileMode.OtherExecute
);
}
UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds...";
await Task.Delay(1000);
UpdateText = "Update complete. Restarting Stability Matrix in 2 seconds...";

332
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -48,52 +48,68 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
private readonly ISharedFolders sharedFolders;
private readonly ServiceManager<ViewModelBase> dialogFactory;
protected readonly IPackageFactory PackageFactory;
// Regex to match if input contains a yes/no prompt,
// i.e "Y/n", "yes/no". Case insensitive.
// Separated by / or |.
[GeneratedRegex(@"y(/|\|)n|yes(/|\|)no", RegexOptions.IgnoreCase)]
private static partial Regex InputYesNoRegex();
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();
[ObservableProperty] private bool launchButtonVisibility;
[ObservableProperty] private bool stopButtonVisibility;
[ObservableProperty] private bool isLaunchTeachingTipsOpen;
[ObservableProperty] private bool showWebUiButton;
[ObservableProperty, NotifyPropertyChangedFor(nameof(SelectedBasePackage),
nameof(SelectedPackageExtraCommands))]
[ObservableProperty]
private bool launchButtonVisibility;
[ObservableProperty]
private bool stopButtonVisibility;
[ObservableProperty]
private bool isLaunchTeachingTipsOpen;
[ObservableProperty]
private bool showWebUiButton;
[
ObservableProperty,
NotifyPropertyChangedFor(nameof(SelectedBasePackage), nameof(SelectedPackageExtraCommands))
]
private InstalledPackage? selectedPackage;
[ObservableProperty] private ObservableCollection<InstalledPackage> installedPackages = new();
[ObservableProperty] private BasePackage? runningPackage;
[ObservableProperty]
private ObservableCollection<InstalledPackage> installedPackages = new();
[ObservableProperty]
private BasePackage? runningPackage;
public virtual BasePackage? SelectedBasePackage =>
PackageFactory.FindPackageByName(SelectedPackage?.PackageName);
public IEnumerable<string> SelectedPackageExtraCommands =>
SelectedBasePackage?.ExtraLaunchCommands ?? Enumerable.Empty<string>();
// private bool clearingPackages;
private string webUiUrl = string.Empty;
// Input info-bars
[ObservableProperty] private bool showManualInputPrompt;
[ObservableProperty] private bool showConfirmInputPrompt;
[ObservableProperty]
private bool showManualInputPrompt;
[ObservableProperty]
private bool showConfirmInputPrompt;
public LaunchPageViewModel(
ILogger<LaunchPageViewModel> logger,
ISettingsManager settingsManager,
ILogger<LaunchPageViewModel> logger,
ISettingsManager settingsManager,
IPackageFactory packageFactory,
IPyRunner pyRunner,
INotificationService notificationService,
IPyRunner pyRunner,
INotificationService notificationService,
ISharedFolders sharedFolders,
ServiceManager<ViewModelBase> dialogFactory)
ServiceManager<ViewModelBase> dialogFactory
)
{
this.logger = logger;
this.settingsManager = settingsManager;
@ -102,11 +118,13 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
this.notificationService = notificationService;
this.sharedFolders = sharedFolders;
this.dialogFactory = dialogFactory;
settingsManager.RelayPropertyFor(this,
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedPackage,
settings => settings.ActiveInstalledPackage);
settings => settings.ActiveInstalledPackage
);
EventManager.Instance.PackageLaunchRequested += OnPackageLaunchRequested;
EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished;
EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
@ -130,9 +148,11 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
{
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.",
NotificationType.Error);
NotificationType.Error
);
return;
}
@ -143,15 +163,19 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
public override void OnLoaded()
{
// Ensure active package either exists or is null
settingsManager.Transaction(s =>
{
s.UpdateActiveInstalledPackage();
}, ignoreMissingLibraryDir: true);
settingsManager.Transaction(
s =>
{
s.UpdateActiveInstalledPackage();
},
ignoreMissingLibraryDir: true
);
// Load installed packages
InstalledPackages =
new ObservableCollection<InstalledPackage>(settingsManager.Settings.InstalledPackages);
InstalledPackages = new ObservableCollection<InstalledPackage>(
settingsManager.Settings.InstalledPackages
);
// Load active package
SelectedPackage = settingsManager.Settings.ActiveInstalledPackage;
}
@ -165,15 +189,18 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
protected virtual async Task LaunchImpl(string? command)
{
IsLaunchTeachingTipsOpen = false;
var activeInstall = SelectedPackage;
if (activeInstall == null)
{
// No selected package: error notification
notificationService.Show(new Notification(
message: "You must install and select a package before launching",
title: "No package selected",
type: NotificationType.Error));
notificationService.Show(
new Notification(
message: "You must install and select a package before launching",
title: "No package selected",
type: NotificationType.Error
)
);
return;
}
@ -186,11 +213,16 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
{
logger.LogWarning(
"During launch, package name '{PackageName}' did not match a definition",
activeInstallName);
notificationService.Show(new Notification("Package name invalid",
"Install package name did not match a definition. Please reinstall and let us know about this issue.",
NotificationType.Error));
activeInstallName
);
notificationService.Show(
new Notification(
"Package name invalid",
"Install package name did not match a definition. Please reinstall and let us know about this issue.",
NotificationType.Error
)
);
return;
}
@ -205,13 +237,13 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
.FromDefinitions(definitions, Array.Empty<LaunchOption>())
.ToImmutableArray();
var args = cards
.SelectMany(c => c.Options)
.ToList();
logger.LogDebug("Setting initial launch args: {Args}",
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr())));
var args = cards.SelectMany(c => c.Options).ToList();
logger.LogDebug(
"Setting initial launch args: {Args}",
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr()))
);
settingsManager.SaveLaunchArgs(activeInstall.Id, args);
}
@ -219,21 +251,23 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Get path from package
var packagePath = new DirectoryPath(settingsManager.LibraryDir, activeInstall.LibraryPath!);
// Unpack sitecustomize.py to venv
await UnpackSiteCustomize(packagePath.JoinDir("venv"));
basePackage.ConsoleOutput += OnProcessOutputReceived;
basePackage.Exited += OnProcessExited;
basePackage.StartupComplete += RunningPackageOnStartupComplete;
// Clear console and start update processing
await Console.StopUpdatesAsync();
await Console.Clear();
Console.StartUpdates();
// 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
var userArgs = settingsManager.GetLaunchArgs(activeInstall.Id);
@ -241,14 +275,16 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Join with extras, if any
userArgsString = string.Join(" ", userArgsString, basePackage.ExtraLaunchArguments);
// Use input command if provided, otherwise use package launch command
command ??= basePackage.LaunchCommand;
await basePackage.RunPackage(packagePath, command, userArgsString);
await basePackage.RunPackage(packagePath, command, userArgsString, OnProcessOutputReceived);
RunningPackage = basePackage;
EventManager.Instance.OnRunningPackageStatusChanged(new PackagePair(activeInstall, basePackage));
EventManager.Instance.OnRunningPackageStatusChanged(
new PackagePair(activeInstall, basePackage)
);
}
// Unpacks sitecustomize.py to the target venv
@ -292,12 +328,12 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Open a config page
var userLaunchArgs = settingsManager.GetLaunchArgs(activeInstall.Id);
var viewModel = dialogFactory.Get<LaunchOptionsViewModel>();
viewModel.Cards = LaunchOptionCard.FromDefinitions(definitions, userLaunchArgs)
viewModel.Cards = LaunchOptionCard
.FromDefinitions(definitions, userLaunchArgs)
.ToImmutableArray();
logger.LogDebug("Launching config dialog with cards: {CardsCount}",
viewModel.Cards.Count);
logger.LogDebug("Launching config dialog with cards: {CardsCount}", viewModel.Cards.Count);
var dialog = new BetterContentDialog
{
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
@ -308,12 +344,9 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
DefaultButton = ContentDialogButton.Primary,
ContentMargin = new Thickness(32, 16),
Padding = new Thickness(0, 16),
Content = new LaunchOptionsDialog
{
DataContext = viewModel,
}
Content = new LaunchOptionsDialog { DataContext = viewModel, }
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
@ -323,7 +356,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
settingsManager.SaveLaunchArgs(activeInstall.Id, args);
}
}
// Send user input to running package
public async Task SendInput(string input)
{
@ -361,7 +394,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
ShowConfirmInputPrompt = false;
}
[RelayCommand]
private async Task SendManualInput(string input)
{
@ -369,75 +402,88 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
Console.PostLine(input);
await SendInput(input);
}
public virtual async Task Stop()
{
if (RunningPackage is null) return;
if (RunningPackage is null)
return;
await RunningPackage.WaitForShutdown();
RunningPackage = null;
ShowWebUiButton = false;
Console.PostLine($"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}");
}
public void OpenWebUi()
{
if (string.IsNullOrEmpty(webUiUrl)) return;
notificationService.TryAsync(Task.Run(() => ProcessRunner.OpenUrl(webUiUrl)),
"Failed to open URL", $"{webUiUrl}");
if (string.IsNullOrEmpty(webUiUrl))
return;
notificationService.TryAsync(
Task.Run(() => ProcessRunner.OpenUrl(webUiUrl)),
"Failed to open URL",
$"{webUiUrl}"
);
}
private void OnProcessExited(object? sender, int exitCode)
{
EventManager.Instance.OnRunningPackageStatusChanged(null);
Dispatcher.UIThread.InvokeAsync(async () =>
{
logger.LogTrace("Process exited ({Code}) at {Time:g}",
exitCode, DateTimeOffset.Now);
// Need to wait for streams to finish before detaching handlers
if (sender is BaseGitPackage {VenvRunner: not null} package)
Dispatcher.UIThread
.InvokeAsync(async () =>
{
var process = package.VenvRunner.Process;
if (process is not null)
logger.LogTrace(
"Process exited ({Code}) at {Time:g}",
exitCode,
DateTimeOffset.Now
);
// Need to wait for streams to finish before detaching handlers
if (sender is BaseGitPackage { VenvRunner: not null } package)
{
// Max 5 seconds
var ct = new CancellationTokenSource(5000).Token;
try
var process = package.VenvRunner.Process;
if (process is not null)
{
await process.WaitUntilOutputEOF(ct);
}
catch (OperationCanceledException e)
{
logger.LogWarning("Waiting for process EOF timed out: {Message}", e.Message);
// Max 5 seconds
var ct = new CancellationTokenSource(5000).Token;
try
{
await process.WaitUntilOutputEOF(ct);
}
catch (OperationCanceledException e)
{
logger.LogWarning(
"Waiting for process EOF timed out: {Message}",
e.Message
);
}
}
}
}
// Detach handlers
if (sender is BasePackage basePackage)
{
basePackage.ConsoleOutput -= OnProcessOutputReceived;
basePackage.Exited -= OnProcessExited;
basePackage.StartupComplete -= RunningPackageOnStartupComplete;
}
RunningPackage = null;
ShowWebUiButton = false;
await Console.StopUpdatesAsync();
// Need to reset cursor in case its in some weird position
// from progress bars
await Console.ResetWriteCursor();
Console.PostLine($"{Environment.NewLine}Process finished with exit code {exitCode}");
}).SafeFireAndForget();
// Detach handlers
if (sender is BasePackage basePackage)
{
basePackage.Exited -= OnProcessExited;
basePackage.StartupComplete -= RunningPackageOnStartupComplete;
}
RunningPackage = null;
ShowWebUiButton = false;
await Console.StopUpdatesAsync();
// Need to reset cursor in case its in some weird position
// from progress bars
await Console.ResetWriteCursor();
Console.PostLine(
$"{Environment.NewLine}Process finished with exit code {exitCode}"
);
})
.SafeFireAndForget();
}
// Callback for processes
private void OnProcessOutputReceived(object? sender, ProcessOutput output)
private void OnProcessOutputReceived(ProcessOutput output)
{
Console.Post(output);
EventManager.Instance.OnScrollToBottomRequested();
@ -447,7 +493,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
{
OnLoaded();
}
private void RunningPackageOnStartupComplete(object? sender, string e)
{
webUiUrl = e.Replace("0.0.0.0", "127.0.0.1");
@ -462,39 +508,45 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
if (e.CloseReason is WindowCloseReason.WindowClosing)
{
e.Cancel = true;
var dialog = CreateExitConfirmDialog();
Dispatcher.UIThread.InvokeAsync(async () =>
{
if ((TaskDialogStandardResult)
await dialog.ShowAsync(true) == TaskDialogStandardResult.Yes)
Dispatcher.UIThread
.InvokeAsync(async () =>
{
App.Services.GetRequiredService<MainWindow>().Hide();
App.Shutdown();
}
}).SafeFireAndForget();
if (
(TaskDialogStandardResult)await dialog.ShowAsync(true)
== TaskDialogStandardResult.Yes
)
{
App.Services.GetRequiredService<MainWindow>().Hide();
App.Shutdown();
}
})
.SafeFireAndForget();
}
}
}
private static TaskDialog CreateExitConfirmDialog()
{
var dialog = DialogHelper.CreateTaskDialog("Confirm Exit",
"Are you sure you want to exit? This will also close the currently running package.");
var dialog = DialogHelper.CreateTaskDialog(
"Confirm Exit",
"Are you sure you want to exit? This will also close the currently running package."
);
dialog.ShowProgressBar = false;
dialog.FooterVisibility = TaskDialogFooterVisibility.Never;
dialog.Buttons = new List<TaskDialogButton>
{
new("Exit", TaskDialogStandardResult.Yes),
TaskDialogButton.CancelButton
};
dialog.Buttons[0].IsDefault = true;
return dialog;
}
public void Dispose()
{
RunningPackage?.Shutdown();
@ -513,7 +565,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
RunningPackage = null;
}
await Console.DisposeAsync();
GC.SuppressFinalize(this);
}
}

62
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -11,6 +11,7 @@ using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
@ -27,13 +28,13 @@ public partial class MainWindowViewModel : ViewModelBase
private readonly ITrackedDownloadService trackedDownloadService;
private readonly IDiscordRichPresenceService discordRichPresenceService;
public string Greeting => "Welcome to Avalonia!";
[ObservableProperty]
private PageViewModelBase? currentPage;
[ObservableProperty]
[ObservableProperty]
private object? selectedCategory;
[ObservableProperty]
private List<PageViewModelBase> pages = new();
@ -44,16 +45,17 @@ public partial class MainWindowViewModel : ViewModelBase
public UpdateViewModel UpdateViewModel { get; init; }
public MainWindowViewModel(
ISettingsManager settingsManager,
ISettingsManager settingsManager,
IDiscordRichPresenceService discordRichPresenceService,
ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService)
ITrackedDownloadService trackedDownloadService
)
{
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.discordRichPresenceService = discordRichPresenceService;
this.trackedDownloadService = trackedDownloadService;
ProgressManagerViewModel = dialogFactory.Get<ProgressManagerViewModel>();
UpdateViewModel = dialogFactory.Get<UpdateViewModel>();
}
@ -61,7 +63,7 @@ public partial class MainWindowViewModel : ViewModelBase
public override void OnLoaded()
{
base.OnLoaded();
// Set only if null, since this may be called again when content dialogs open
CurrentPage ??= Pages.FirstOrDefault();
SelectedCategory ??= Pages.FirstOrDefault();
@ -72,24 +74,25 @@ public partial class MainWindowViewModel : ViewModelBase
await base.OnLoadedAsync();
// Skip if design mode
if (Design.IsDesignMode) return;
if (Design.IsDesignMode)
return;
if (!await EnsureDataDirectory())
{
// False if user exited dialog, shutdown app
App.Shutdown();
return;
}
// Initialize Discord Rich Presence (this needs LibraryDir so is set here)
discordRichPresenceService.UpdateState();
// Load in-progress downloads
ProgressManagerViewModel.AddDownloads(trackedDownloadService.Downloads);
// Index checkpoints if we dont have
Task.Run(() => settingsManager.IndexCheckpoints()).SafeFireAndForget();
if (!settingsManager.Settings.InstalledPackages.Any())
{
var viewModel = dialogFactory.Get<OneClickInstallViewModel>();
@ -98,17 +101,15 @@ public partial class MainWindowViewModel : ViewModelBase
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new OneClickInstallDialog
{
DataContext = viewModel
},
Content = new OneClickInstallDialog { DataContext = viewModel },
};
EventManager.Instance.OneClickInstallFinished += (_, skipped) =>
{
dialog.Hide();
if (skipped) return;
if (skipped)
return;
EventManager.Instance.OnTeachingTooltipNeeded();
};
@ -125,24 +126,25 @@ public partial class MainWindowViewModel : ViewModelBase
if (!settingsManager.TryFindLibrary())
{
var result = await ShowSelectDataDirectoryDialog();
if (!result) return false;
if (!result)
return false;
}
// Try to find library again, should be found now
if (!settingsManager.TryFindLibrary())
{
throw new Exception("Could not find library after setting path");
}
// Tell LaunchPage to load any packages if they selected an existing directory
EventManager.Instance.OnInstalledPackagesChanged();
// Check if there are old packages, if so show migration dialog
// TODO: Migration dialog
return true;
}
/// <summary>
/// Shows the select data directory dialog.
/// </summary>
@ -155,10 +157,7 @@ public partial class MainWindowViewModel : ViewModelBase
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new SelectDataDirectoryDialog
{
DataContext = viewModel
}
Content = new SelectDataDirectoryDialog { DataContext = viewModel }
};
var result = await dialog.ShowAsync();
@ -191,10 +190,7 @@ public partial class MainWindowViewModel : ViewModelBase
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new UpdateDialog
{
DataContext = viewModel
}
Content = new UpdateDialog { DataContext = viewModel }
};
await dialog.ShowAsync();

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

@ -34,19 +34,29 @@ public partial class PackageCardViewModel : ProgressViewModel
private readonly INavigationService navigationService;
private readonly ServiceManager<ViewModelBase> vmFactory;
[ObservableProperty] private InstalledPackage? package;
[ObservableProperty] private string? cardImageSource;
[ObservableProperty] private bool isUpdateAvailable;
[ObservableProperty] private string? installedVersion;
[ObservableProperty] private bool isUnknownPackage;
[ObservableProperty]
private InstalledPackage? package;
[ObservableProperty]
private string? cardImageSource;
[ObservableProperty]
private bool isUpdateAvailable;
[ObservableProperty]
private string? installedVersion;
[ObservableProperty]
private bool isUnknownPackage;
public PackageCardViewModel(
ILogger<PackageCardViewModel> logger,
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
INotificationService notificationService,
ISettingsManager settingsManager,
INavigationService navigationService,
ServiceManager<ViewModelBase> vmFactory)
ServiceManager<ViewModelBase> vmFactory
)
{
this.logger = logger;
this.packageFactory = packageFactory;
@ -70,11 +80,10 @@ public partial class PackageCardViewModel : ProgressViewModel
else
{
IsUnknownPackage = false;
var basePackage = packageFactory[value.PackageName];
CardImageSource = basePackage?.PreviewImageUri.ToString()
?? Assets.NoImage.ToString();
InstalledVersion = value.DisplayVersion ?? "Unknown";
CardImageSource = basePackage?.PreviewImageUri.ToString() ?? Assets.NoImage.ToString();
InstalledVersion = value.Version?.DisplayVersion ?? "Unknown";
}
}
@ -87,13 +96,13 @@ public partial class PackageCardViewModel : ProgressViewModel
{
if (Package == null)
return;
settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id);
navigationService.NavigateTo<LaunchPageViewModel>(new BetterDrillInNavigationTransition());
EventManager.Instance.OnPackageLaunchRequested(Package.Id);
}
public async Task Uninstall()
{
if (Package?.LibraryPath == null)
@ -104,7 +113,8 @@ public partial class PackageCardViewModel : ProgressViewModel
var dialog = new ContentDialog
{
Title = "Are you sure?",
Content = "This will delete all folders in the package directory, including any generated images in that directory as well as any files you may have added.",
Content =
"This will delete all folders in the package directory, including any generated images in that directory as well as any files you may have added.",
PrimaryButtonText = "Yes, delete it",
CloseButtonText = "No, keep it",
DefaultButton = ContentDialogButton.Primary
@ -119,14 +129,20 @@ public partial class PackageCardViewModel : ProgressViewModel
var packagePath = new DirectoryPath(settingsManager.LibraryDir, Package.LibraryPath);
var deleteTask = packagePath.DeleteVerboseAsync(logger);
var taskResult = await notificationService.TryAsync(deleteTask,
"Some files could not be deleted. Please close any open files in the package directory and try again.");
var taskResult = await notificationService.TryAsync(
deleteTask,
"Some files could not be deleted. Please close any open files in the package directory and try again."
);
if (taskResult.IsSuccessful)
{
notificationService.Show(new Notification("Success",
$"Package {Package.DisplayName} uninstalled",
NotificationType.Success));
notificationService.Show(
new Notification(
"Success",
$"Package {Package.DisplayName} uninstalled",
NotificationType.Success
)
);
if (!IsUnknownPackage)
{
@ -135,79 +151,105 @@ public partial class PackageCardViewModel : ProgressViewModel
settings.RemoveInstalledPackageAndUpdateActive(Package);
});
}
EventManager.Instance.OnInstalledPackagesChanged();
}
}
}
public async Task Update()
{
if (Package is null || IsUnknownPackage) return;
if (Package is null || IsUnknownPackage)
return;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
{
logger.LogWarning("Could not find package {SelectedPackagePackageName}",
Package.PackageName);
notificationService.Show("Invalid Package type",
logger.LogWarning(
"Could not find package {SelectedPackagePackageName}",
Package.PackageName
);
notificationService.Show(
"Invalid Package type",
$"Package {Package.PackageName.ToRepr()} is not a valid package type",
NotificationType.Error);
NotificationType.Error
);
return;
}
var packageName = Package.DisplayName ?? Package.PackageName ?? "";
Text = $"Updating {packageName}";
IsIndeterminate = true;
var progressId = Guid.NewGuid();
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName ?? Package.PackageName!,
new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update)));
EventManager.Instance.OnProgressChanged(
new ProgressItem(
progressId,
Package.DisplayName ?? Package.PackageName!,
new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update)
)
);
try
{
basePackage.InstallLocation = Package.FullPath!;
var progress = new Progress<ProgressReport>(progress =>
{
var percent = Convert.ToInt32(progress.Percentage);
Value = percent;
IsIndeterminate = progress.IsIndeterminate;
Text = $"Updating {Package.DisplayName}";
EventManager.Instance.OnGlobalProgressChanged(percent);
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
packageName, progress));
EventManager.Instance.OnProgressChanged(
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);
notificationService.Show("Update complete",
notificationService.Show(
"Update complete",
$"{Package.DisplayName} has been updated to the latest version.",
NotificationType.Success);
NotificationType.Success
);
await using (settingsManager.BeginTransaction())
{
Package.UpdateAvailable = false;
}
IsUpdateAvailable = false;
InstalledVersion = Package.DisplayVersion ?? "Unknown";
InstalledVersion = updateResult.DisplayVersion ?? "Unknown";
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
packageName,
new ProgressReport(1f, "Update complete", type: ProgressType.Update)));
EventManager.Instance.OnProgressChanged(
new ProgressItem(
progressId,
packageName,
new ProgressReport(1f, "Update complete", type: ProgressType.Update)
)
);
}
catch (Exception e)
{
logger.LogError(e, "Error Updating Package ({PackageName})", basePackage.Name);
notificationService.ShowPersistent($"Error Updating {Package.DisplayName}", e.Message, NotificationType.Error);
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
packageName,
new ProgressReport(0f, "Update failed", type: ProgressType.Update), Failed: true));
notificationService.ShowPersistent(
$"Error Updating {Package.DisplayName}",
e.Message,
NotificationType.Error
);
EventManager.Instance.OnProgressChanged(
new ProgressItem(
progressId,
packageName,
new ProgressReport(0f, "Update failed", type: ProgressType.Update),
Failed: true
)
);
}
finally
{
@ -219,27 +261,23 @@ public partial class PackageCardViewModel : ProgressViewModel
public async Task Import()
{
if (!IsUnknownPackage || Design.IsDesignMode) return;
if (!IsUnknownPackage || Design.IsDesignMode)
return;
var viewModel = vmFactory.Get<PackageImportViewModel>(vm =>
{
vm.PackagePath =
new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException());
vm.PackagePath = new DirectoryPath(
Package?.FullPath ?? throw new InvalidOperationException()
);
});
var dialog = new TaskDialog
{
Content = new PackageImportDialog
{
DataContext = viewModel
},
Content = new PackageImportDialog { DataContext = viewModel },
ShowProgressBar = false,
Buttons = new List<TaskDialogButton>
{
new(Resources.Action_Import, TaskDialogStandardResult.Yes)
{
IsDefault = true
},
new(Resources.Action_Import, TaskDialogStandardResult.Yes) { IsDefault = true },
new(Resources.Action_Cancel, TaskDialogStandardResult.Cancel)
}
};
@ -256,7 +294,9 @@ public partial class PackageCardViewModel : ProgressViewModel
await using (new MinimumDelay(200, 300))
{
var result = await notificationService.TryAsync(viewModel.AddPackageWithCurrentInputs());
var result = await notificationService.TryAsync(
viewModel.AddPackageWithCurrentInputs()
);
if (result.IsSuccessful)
{
EventManager.Instance.OnInstalledPackagesChanged();
@ -268,18 +308,18 @@ public partial class PackageCardViewModel : ProgressViewModel
};
dialog.XamlRoot = App.VisualRoot;
await dialog.ShowAsync(true);
}
public async Task OpenFolder()
{
if (string.IsNullOrWhiteSpace(Package?.FullPath))
return;
await ProcessRunner.OpenFolderBrowser(Package.FullPath);
}
private async Task<bool> HasUpdate()
{
if (Package == null || IsUnknownPackage || Design.IsDesignMode)
@ -289,8 +329,9 @@ public partial class PackageCardViewModel : ProgressViewModel
if (basePackage == null)
return false;
var canCheckUpdate = Package.LastUpdateCheck == null ||
Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15);
var canCheckUpdate =
Package.LastUpdateCheck == null
|| Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15);
if (!canCheckUpdate)
{

67
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
@ -21,6 +22,7 @@ using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -37,6 +39,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly INotificationService notificationService;
public override string Title => "Packages";
public override IconSource IconSource =>
@ -61,12 +64,14 @@ public partial class PackageManagerViewModel : PageViewModelBase
public PackageManagerViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
ServiceManager<ViewModelBase> dialogFactory
ServiceManager<ViewModelBase> dialogFactory,
INotificationService notificationService
)
{
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
this.dialogFactory = dialogFactory;
this.notificationService = notificationService;
EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
@ -77,11 +82,14 @@ public partial class PackageManagerViewModel : PageViewModelBase
.Or(unknown)
.DeferUntilLoaded()
.Bind(Packages)
.Transform(p => dialogFactory.Get<PackageCardViewModel>(vm =>
{
vm.Package = p;
vm.OnLoadedAsync().SafeFireAndForget();
}))
.Transform(
p =>
dialogFactory.Get<PackageCardViewModel>(vm =>
{
vm.Package = p;
vm.OnLoadedAsync().SafeFireAndForget();
})
)
.Bind(PackageCards)
.Subscribe();
}
@ -100,8 +108,11 @@ public partial class PackageManagerViewModel : PageViewModelBase
{
if (Design.IsDesignMode)
return;
installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer);
installedPackages.EditDiff(
settingsManager.Settings.InstalledPackages,
InstalledPackage.Comparer
);
var currentUnknown = await Task.Run(IndexUnknownPackages);
unknownInstalledPackages.Edit(s => s.Load(currentUnknown));
@ -115,7 +126,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
var dialog = new BetterContentDialog
{
MaxDialogWidth = 1100,
MaxDialogWidth = 900,
MinDialogWidth = 900,
DefaultButton = ContentDialogButton.Close,
IsPrimaryButtonEnabled = false,
@ -124,33 +135,53 @@ public partial class PackageManagerViewModel : PageViewModelBase
Content = new InstallerDialog { DataContext = viewModel }
};
await dialog.ShowAsync();
await OnLoadedAsync();
var result = await dialog.ShowAsync();
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()
{
{
var packageDir = new DirectoryPath(settingsManager.LibraryDir).JoinDir("Packages");
if (!packageDir.Exists)
{
yield break;
}
var currentPackages = settingsManager.Settings.InstalledPackages.ToImmutableArray();
foreach (var subDir in packageDir.Info
.EnumerateDirectories()
.Select(info => new DirectoryPath(info)))
foreach (
var subDir in packageDir.Info
.EnumerateDirectories()
.Select(info => new DirectoryPath(info))
)
{
var expectedLibraryPath = $"Packages{Path.DirectorySeparatorChar}{subDir.Name}";
// Skip if the package is already installed
if (currentPackages.Any(p => p.LibraryPath == expectedLibraryPath))
{
continue;
}
if (settingsManager.PackageInstallsInProgress.Contains(subDir.Name))
{
continue;
}
yield return UnknownInstalledPackage.FromDirectoryName(subDir.Name);
}
}

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

@ -4,13 +4,13 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.Progress;
public class DownloadProgressItemViewModel : PausableProgressItemViewModelBase
{
private readonly TrackedDownload download;
public DownloadProgressItemViewModel(TrackedDownload download)
public DownloadProgressItemViewModel(TrackedDownload download)
{
this.download = download;

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 CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.Progress;
public class ProgressItemViewModel : ProgressItemViewModelBase
{

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

@ -1,8 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
@ -10,48 +13,67 @@ using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.Progress;
[View(typeof(ProgressManagerPage))]
public partial class ProgressManagerViewModel : PageViewModelBase
{
private readonly INotificationService notificationService;
public override string Title => "Download Manager";
public override IconSource IconSource => new SymbolIconSource {Symbol = Symbol.ArrowCircleDown, IsFilled = true};
public override string Title => "Download Manager";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.ArrowCircleDown, IsFilled = true };
public AvaloniaList<ProgressItemViewModelBase> ProgressItems { get; } = new();
[ObservableProperty]
private bool isOpen;
public ProgressManagerViewModel(
ITrackedDownloadService trackedDownloadService,
INotificationService notificationService)
INotificationService notificationService
)
{
this.notificationService = notificationService;
// Attach to the event
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)
{
var vm = new DownloadProgressItemViewModel(e);
// Attach notification handlers
e.ProgressStateChanged += (s, state) =>
{
var download = s as TrackedDownload;
switch (state)
{
case ProgressState.Success:
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;
case ProgressState.Failed:
@ -62,18 +84,26 @@ public partial class ProgressManagerViewModel : PageViewModelBase
}
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;
case ProgressState.Cancelled:
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;
}
};
ProgressItems.Add(vm);
}
@ -89,12 +119,24 @@ public partial class ProgressManagerViewModel : PageViewModelBase
ProgressItems.Add(vm);
}
}
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)
{
notificationService.ShowPersistent(title, message, NotificationType.Error);
}
public void StartEventListener()
{
EventManager.Instance.ProgressChanged += OnProgressChanged;

322
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -43,7 +43,7 @@ namespace StabilityMatrix.Avalonia.ViewModels;
public partial class SettingsViewModel : PageViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly IPrerequisiteHelper prerequisiteHelper;
@ -51,72 +51,74 @@ public partial class SettingsViewModel : PageViewModelBase
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ITrackedDownloadService trackedDownloadService;
private readonly IModelIndexService modelIndexService;
public SharedState SharedState { get; }
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
public string AppVersion => $"Version {Compat.AppVersion}" +
(Program.IsDebugBuild ? " (Debug)" : "");
public string AppVersion =>
$"Version {Compat.AppVersion}" + (Program.IsDebugBuild ? " (Debug)" : "");
// Theme section
[ObservableProperty] private string? selectedTheme;
public IReadOnlyList<string> AvailableThemes { get; } = new[]
{
"Light",
"Dark",
"System",
};
[ObservableProperty]
private string? selectedTheme;
public IReadOnlyList<string> AvailableThemes { get; } = new[] { "Light", "Dark", "System", };
[ObservableProperty] private CultureInfo selectedLanguage;
[ObservableProperty]
private CultureInfo selectedLanguage;
// ReSharper disable once MemberCanBeMadeStatic.Global
public IReadOnlyList<CultureInfo> AvailableLanguages => Cultures.SupportedCultures;
public IReadOnlyList<float> AnimationScaleOptions { get; } = new[]
{
0f,
0.25f,
0.5f,
0.75f,
1f,
1.25f,
1.5f,
1.75f,
2f,
};
[ObservableProperty] private float selectedAnimationScale;
public IReadOnlyList<float> AnimationScaleOptions { get; } =
new[] { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f, };
[ObservableProperty]
private float selectedAnimationScale;
// Shared folder options
[ObservableProperty] private bool removeSymlinksOnShutdown;
[ObservableProperty]
private bool removeSymlinksOnShutdown;
// Integrations section
[ObservableProperty] private bool isDiscordRichPresenceEnabled;
[ObservableProperty]
private bool isDiscordRichPresenceEnabled;
// Debug section
[ObservableProperty] private string? debugPaths;
[ObservableProperty] private string? debugCompatInfo;
[ObservableProperty] private string? debugGpuInfo;
[ObservableProperty]
private string? debugPaths;
[ObservableProperty]
private string? debugCompatInfo;
[ObservableProperty]
private string? debugGpuInfo;
// Info section
private const int VersionTapCountThreshold = 7;
[ObservableProperty, NotifyPropertyChangedFor(nameof(VersionFlyoutText))] private int versionTapCount;
[ObservableProperty] private bool isVersionTapTeachingTipOpen;
public string VersionFlyoutText => $"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options.";
[ObservableProperty, NotifyPropertyChangedFor(nameof(VersionFlyoutText))]
private int versionTapCount;
[ObservableProperty]
private bool isVersionTapTeachingTipOpen;
public string VersionFlyoutText =>
$"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options.";
public SettingsViewModel(
INotificationService notificationService,
INotificationService notificationService,
ISettingsManager settingsManager,
IPrerequisiteHelper prerequisiteHelper,
IPyRunner pyRunner,
ServiceManager<ViewModelBase> dialogFactory,
SharedState sharedState,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService)
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService
)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
@ -127,29 +129,32 @@ public partial class SettingsViewModel : PageViewModelBase
this.modelIndexService = modelIndexService;
SharedState = sharedState;
SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1];
SelectedLanguage = Cultures.GetSupportedCultureOrDefault(settingsManager.Settings.Language);
RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown;
SelectedAnimationScale = settingsManager.Settings.AnimationScale;
settingsManager.RelayPropertyFor(this,
vm => vm.SelectedTheme,
settings => settings.Theme);
settingsManager.RelayPropertyFor(this,
settingsManager.RelayPropertyFor(this, vm => vm.SelectedTheme, settings => settings.Theme);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled);
settingsManager.RelayPropertyFor(this,
settings => settings.IsDiscordRichPresenceEnabled
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale);
settings => settings.AnimationScale
);
}
partial void OnSelectedThemeChanged(string? value)
{
// In case design / tests
if (Application.Current is null) return;
if (Application.Current is null)
return;
// Change theme
Application.Current.RequestedThemeVariant = value switch
{
@ -161,16 +166,16 @@ public partial class SettingsViewModel : PageViewModelBase
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
if (AvailableLanguages.Contains(newValue))
{
Logger.Info("Changing language from {Old} to {New}",
oldValue, newValue);
Logger.Info("Changing language from {Old} to {New}", oldValue, newValue);
Cultures.TrySetSupportedCulture(newValue);
settingsManager.Transaction(s => s.Language = newValue.Name);
var dialog = new BetterContentDialog
{
Title = Resources.Label_RelaunchRequired,
@ -191,11 +196,14 @@ public partial class SettingsViewModel : PageViewModelBase
}
else
{
Logger.Info("Requested invalid language change from {Old} to {New}",
oldValue, newValue);
Logger.Info(
"Requested invalid language change from {Old} to {New}",
oldValue,
newValue
);
}
}
partial void OnRemoveSymlinksOnShutdownChanged(bool value)
{
settingsManager.Transaction(s => s.RemoveFolderLinksOnShutdown = value);
@ -205,29 +213,30 @@ public partial class SettingsViewModel : PageViewModelBase
{
settingsManager.Transaction(s => s.InstalledModelHashes = new HashSet<string>());
await Task.Run(() => settingsManager.IndexCheckpoints());
notificationService.Show("Checkpoint cache reset", "The checkpoint cache has been reset.",
NotificationType.Success);
notificationService.Show(
"Checkpoint cache reset",
"The checkpoint cache has been reset.",
NotificationType.Success
);
}
#region Package Environment
[RelayCommand]
private async Task OpenEnvVarsDialog()
{
var viewModel = dialogFactory.Get<EnvVarsViewModel>();
// Load current settings
var current = settingsManager.Settings.EnvironmentVariables
?? new Dictionary<string, string>();
var current =
settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
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
{
Content = new EnvVarsDialog
{
DataContext = viewModel
},
Content = new EnvVarsDialog { DataContext = viewModel },
PrimaryButtonText = "Save",
IsPrimaryButtonEnabled = true,
CloseButtonText = "Cancel",
@ -275,7 +284,7 @@ public partial class SettingsViewModel : PageViewModelBase
dialog.PrimaryButtonText = "Ok";
await dialog.ShowAsync();
}
#endregion
#region System
@ -288,27 +297,29 @@ public partial class SettingsViewModel : PageViewModelBase
{
if (!Compat.IsWindows)
{
notificationService.Show(
"Not supported", "This feature is only supported on Windows.");
notificationService.Show("Not supported", "This feature is only supported on Windows.");
return;
}
await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
"Programs");
"Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
var iconPath = shortcutDir.JoinFile("Stability Matrix.ico");
await Assets.AppIcon.ExtractTo(iconPath);
WindowsShortcuts.CreateShortcut(
shortcutLink, appPath, iconPath, "Stability Matrix");
notificationService.Show("Added to Start Menu",
"Stability Matrix has been added to the Start Menu.", NotificationType.Success);
WindowsShortcuts.CreateShortcut(shortcutLink, appPath, iconPath, "Stability Matrix");
notificationService.Show(
"Added to Start Menu",
"Stability Matrix has been added to the Start Menu.",
NotificationType.Success
);
}
/// <summary>
@ -320,54 +331,62 @@ public partial class SettingsViewModel : PageViewModelBase
{
if (!Compat.IsWindows)
{
notificationService.Show(
"Not supported", "This feature is only supported on Windows.");
notificationService.Show("Not supported", "This feature is only supported on Windows.");
return;
}
// Confirmation dialog
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?",
PrimaryButtonText = "Yes",
CloseButtonText = "Cancel",
DefaultButton = ContentDialogButton.Primary
};
if (await dialog.ShowAsync() != ContentDialogResult.Primary)
{
return;
}
await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs");
"Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
var iconPath = shortcutDir.JoinFile("Stability Matrix.ico");
// We can't directly write to the targets, so extract to temporary directory first
using var tempDir = new TempDirectoryPath();
await Assets.AppIcon.ExtractTo(tempDir.JoinFile("Stability Matrix.ico"));
WindowsShortcuts.CreateShortcut(
tempDir.JoinFile("Stability Matrix.lnk"), appPath, iconPath,
"Stability Matrix");
tempDir.JoinFile("Stability Matrix.lnk"),
appPath,
iconPath,
"Stability Matrix"
);
// Move to target
try
{
var moveLinkResult = await WindowsElevated.MoveFiles(
(tempDir.JoinFile("Stability Matrix.lnk"), shortcutLink),
(tempDir.JoinFile("Stability Matrix.ico"), iconPath));
(tempDir.JoinFile("Stability Matrix.ico"), iconPath)
);
if (moveLinkResult != 0)
{
notificationService.ShowPersistent("Failed to create shortcut", $"Could not copy shortcut",
NotificationType.Error);
notificationService.ShowPersistent(
"Failed to create shortcut",
$"Could not copy shortcut",
NotificationType.Error
);
}
}
catch (Win32Exception e)
@ -377,9 +396,12 @@ public partial class SettingsViewModel : PageViewModelBase
notificationService.Show("Could not create shortcut", "", NotificationType.Warning);
return;
}
notificationService.Show("Added to Start Menu",
"Stability Matrix has been added to the Start Menu for all users.", NotificationType.Success);
notificationService.Show(
"Added to Start Menu",
"Stability Matrix has been added to the Start Menu for all users.",
NotificationType.Success
);
}
public async Task PickNewDataDirectory()
@ -390,10 +412,7 @@ public partial class SettingsViewModel : PageViewModelBase
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new SelectDataDirectoryDialog
{
DataContext = viewModel
}
Content = new SelectDataDirectoryDialog { DataContext = viewModel }
};
var result = await dialog.ShowAsync();
@ -409,7 +428,7 @@ public partial class SettingsViewModel : PageViewModelBase
{
settingsManager.SetLibraryPath(viewModel.DataDirectory);
}
// Restart
var restartDialog = new BetterContentDialog
{
@ -420,14 +439,14 @@ public partial class SettingsViewModel : PageViewModelBase
IsSecondaryButtonEnabled = false,
};
await restartDialog.ShowAsync();
Process.Start(Compat.AppCurrentPath);
App.Shutdown();
}
}
#endregion
#region Debug Section
public void LoadDebugInfo()
{
@ -443,12 +462,12 @@ public partial class SettingsViewModel : PageViewModelBase
AppData Directory [SpecialFolder.ApplicationData]
"{appData}"
""";
// 1. Check portable mode
var appDir = Compat.AppCurrentDir;
var expectedPortableFile = Path.Combine(appDir, "Data", ".sm-portable");
var isPortableMode = File.Exists(expectedPortableFile);
DebugCompatInfo = $"""
Platform: {Compat.Platform}
AppData: {Compat.AppData}
@ -461,24 +480,27 @@ public partial class SettingsViewModel : PageViewModelBase
IsLibraryDirSet = {settingsManager.IsLibraryDirSet}
IsPortableMode = {settingsManager.IsPortableMode}
""";
// Get Gpu info
var gpuInfo = "";
foreach (var (i, gpu) in HardwareHelper.IterGpuInfo().Enumerate())
{
gpuInfo += $"[{i+1}] {gpu}\n";
gpuInfo += $"[{i + 1}] {gpu}\n";
}
DebugGpuInfo = gpuInfo;
}
// Debug buttons
[RelayCommand]
private void DebugNotification()
{
notificationService.Show(new Notification(
title: "Test Notification",
message: "Here is some message",
type: NotificationType.Information));
notificationService.Show(
new Notification(
title: "Test Notification",
message: "Here is some message",
type: NotificationType.Information
)
);
}
[RelayCommand]
@ -493,8 +515,7 @@ public partial class SettingsViewModel : PageViewModelBase
};
var result = await dialog.ShowAsync();
notificationService.Show(new Notification("Content dialog closed",
$"Result: {result}"));
notificationService.Show(new Notification("Content dialog closed", $"Result: {result}"));
}
[RelayCommand]
@ -509,20 +530,14 @@ public partial class SettingsViewModel : PageViewModelBase
{
await modelIndexService.RefreshIndex();
}
[RelayCommand]
private async Task DebugTrackedDownload()
{
var textFields = new TextBoxField[]
{
new()
{
Label = "Url",
},
new()
{
Label = "File path"
}
new() { Label = "Url", },
new() { Label = "File path" }
};
var dialog = DialogHelper.CreateTextEntryDialog("Add download", "", textFields);
@ -542,10 +557,11 @@ public partial class SettingsViewModel : PageViewModelBase
public void OnVersionClick()
{
// Ignore if already enabled
if (SharedState.IsDebugMode) return;
if (SharedState.IsDebugMode)
return;
VersionTapCount++;
switch (VersionTapCount)
{
// Reached required threshold
@ -555,7 +571,9 @@ public partial class SettingsViewModel : PageViewModelBase
// Enable debug options
SharedState.IsDebugMode = true;
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;
break;
}
@ -579,8 +597,11 @@ public partial class SettingsViewModel : PageViewModelBase
}
catch (Exception e)
{
notificationService.Show("Failed to read licenses information",
$"{e}", NotificationType.Error);
notificationService.Show(
"Failed to read licenses information",
$"{e}",
NotificationType.Error
);
}
}
@ -588,15 +609,17 @@ public partial class SettingsViewModel : PageViewModelBase
{
// Read licenses.json
using var reader = new StreamReader(Assets.LicensesJson.Open());
var licenses = JsonSerializer
.Deserialize<IReadOnlyList<LicenseInfo>>(reader.ReadToEnd()) ??
throw new InvalidOperationException("Failed to read licenses.json");
var licenses =
JsonSerializer.Deserialize<IReadOnlyList<LicenseInfo>>(reader.ReadToEnd())
?? throw new InvalidOperationException("Failed to read licenses.json");
// Generate markdown
var builder = new StringBuilder();
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(license.Description);
builder.AppendLine();
@ -608,5 +631,4 @@ public partial class SettingsViewModel : PageViewModelBase
}
#endregion
}

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

@ -1,249 +1,228 @@
<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:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;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:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
x:DataType="dialogs:InstallerViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="500"
d:DataContext="{x:Static mocks:DesignData.InstallerViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.InstallerDialog">
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:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
x:DataType="dialogs:InstallerViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700"
d:DataContext="{x:Static mocks:DesignData.InstallerViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.InstallerDialog">
<Grid RowDefinitions="Auto,Auto,Auto,*">
<!-- Close button -->
<Grid HorizontalAlignment="Right">
<Button
Margin="0,8,8,0"
Classes="transparent"
IsEnabled="{Binding !InstallProgress.IsProgressVisible}"
Command="{Binding OnCloseButtonClick}"
icons:Attached.Icon="fa-solid fa-xmark"/>
<Grid MaxHeight="900"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"
ColumnDefinitions="*,Auto">
<!-- Title and image -->
<Grid Grid.Row="0" Grid.Column="0"
Margin="16, 0, 0, 0"
RowDefinitions="Auto, Auto, Auto, Auto, Auto">
<TextBlock Grid.Row="0" Text="{Binding SelectedPackage.DisplayName}"
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>
<StackPanel
Grid.Row="1"
Margin="16,8,16,0"
Orientation="Vertical"
DataContext="{Binding InstallProgress}"
IsVisible="{Binding IsProgressVisible}">
<TextBlock
HorizontalAlignment="Center"
Padding="8"
Text="{Binding Text}" />
<ProgressBar
IsIndeterminate="{Binding IsIndeterminate}"
Maximum="100"
Width="500"
Value="{Binding Value}" />
<TextBlock
FontSize="10"
HorizontalAlignment="Center"
Padding="4"
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}">
<Grid Grid.Column="0" Grid.Row="2"
Grid.ColumnSpan="2"
Margin="16,0,16,0"
RowDefinitions="Auto, Auto, Auto, Auto"
HorizontalAlignment="Center"
ColumnDefinitions="Auto, Auto, Auto, Auto">
<!--<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>
<Label Grid.Row="1" Grid.Column="0"
Margin="8,0"
Content="Display Name" />
<TextBox Grid.Row="2" Grid.Column="0"
MinWidth="250"
Margin="8,0"
VerticalAlignment="Stretch"
VerticalContentAlignment="Center"
Text="{Binding InstallName, Mode=TwoWay}" />
<StackPanel
MinWidth="400"
Grid.Column="1"
Margin="8,16,0,16"
Orientation="Vertical">
<StackPanel Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="4"
Orientation="Horizontal"
Margin="8"
IsVisible="{Binding ShowDuplicateWarning}">
<ui:SymbolIcon
Foreground="{DynamicResource ThemeRedColor}"
Margin="0,0,8,0"
Symbol="Alert" />
<TextBlock
FontSize="24"
FontWeight="Bold"
Text="{Binding SelectedPackage.DisplayName, FallbackValue=Stable Diffusion Web UI}" />
<TextBlock FontSize="12" Text="{Binding SelectedPackage.ByAuthor, FallbackValue=by Automatic111}" />
Foreground="{DynamicResource ThemeRedColor}"
TextAlignment="Left"
TextWrapping="Wrap">
<Run Text="An installation with this name already exists." />
<LineBreak />
<Run Text="Please choose a different name or select a different Install Location." />
</TextBlock>
</StackPanel>
<Button
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>
<!-- Version Selector -->
<Label Grid.Row="1" Grid.Column="1"
Content="{Binding ReleaseLabelText}" />
<ui:FAComboBox Grid.Row="2" Grid.Column="1"
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 Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4"
HorizontalAlignment="Center"
Orientation="Horizontal" Margin="0,8,0,8"
IsVisible="{Binding IsReleaseModeAvailable}">
<ToggleButton
Content="Releases"
IsChecked="{Binding IsReleaseMode}"
IsEnabled="{Binding IsReleaseModeAvailable}" />
<ToggleButton
Content="Branches"
IsChecked="{Binding !IsReleaseMode}"
Margin="8,0,0,0" />
</StackPanel>
<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>
</ui:HyperlinkButton>
<TextBlock Text="{Binding SelectedPackage.Disclaimer}"
Padding="4"
Foreground="OrangeRed"
TextWrapping="Wrap"
IsVisible="{Binding SelectedPackage.Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<!-- Advanced Options Button -->
<Button Grid.Row="2" Grid.Column="2" VerticalAlignment="Stretch"
Margin="8,0">
<ui:SymbolIcon FontSize="16" Margin="4" Symbol="Settings" />
<Button.Flyout>
<Flyout>
<!-- Advanced Options -->
<StackPanel Orientation="Vertical"
Margin="8"
IsVisible="{Binding !IsAdvancedMode}">
<StackPanel Orientation="Horizontal" Margin="0,8,0,0"
IsVisible="{Binding IsReleaseModeAvailable}">
<ToggleButton
Content="Releases"
IsChecked="{Binding IsReleaseMode}"
IsEnabled="{Binding IsReleaseModeAvailable}" />
<ToggleButton
Content="Branches"
IsChecked="{Binding !IsReleaseMode}"
Margin="8,0,0,0" />
</StackPanel>
<Label Content="Advanced Options"
FontSize="18"
HorizontalAlignment="Center"
Margin="8,0,8,8" />
<StackPanel Margin="0,16,0,0" Orientation="Horizontal">
<StackPanel Orientation="Vertical">
<Label Content="{Binding ReleaseLabelText}" />
<ComboBox
ItemsSource="{Binding AvailableVersions}"
MinWidth="200"
SelectedItem="{Binding SelectedVersion}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:PackageVersion}">
<StackPanel Margin="8,0,0,0" VerticalAlignment="Top">
<Label Content="Commit"
Margin="0,0,0,4"
IsVisible="{Binding !IsReleaseMode}" />
<ui:FAComboBox
IsVisible="{Binding !IsReleaseMode}"
ItemsSource="{Binding AvailableCommits}"
MinWidth="200"
MinHeight="38"
SelectedItem="{Binding SelectedCommit}">
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type database:GitCommit}">
<TextBlock
Margin="0,4,0,4"
Margin="8,4,0,4"
Name="NameTextBlock"
Text="{Binding TagName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
Text="{Binding ShortSha}" />
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
<StackPanel
Margin="8,0,0,0"
Orientation="Vertical"
IsVisible="{Binding !IsReleaseMode}">
<Label Content="Commit" />
<ComboBox
ItemsSource="{Binding AvailableCommits}"
MinWidth="100"
SelectedItem="{Binding SelectedCommit}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type database:GitCommit}">
<StackPanel Margin="8,0,0,0" VerticalAlignment="Top">
<Label Content="Shared Model Folder Strategy"
Margin="0,8,0,4" />
<ui:FAComboBox
ItemsSource="{Binding SelectedPackage.AvailableSharedFolderMethods}"
MinWidth="200"
MinHeight="38"
SelectedItem="{Binding SelectedSharedFolderMethod}">
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:SharedFolderMethod}">
<TextBlock
Margin="0,4,0,4"
Name="NameTextBlock"
Text="{Binding Sha}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</StackPanel>
Margin="8,4,0,4"
Text="{Binding }" />
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
<Label Content="PyTorch Version"
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>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<Label Content="Display Name" Margin="0,16,0,0" />
<StackPanel Orientation="Horizontal" IsVisible="{Binding ShowDuplicateWarning}">
<ui:SymbolIcon
Foreground="{DynamicResource ThemeRedColor}"
Margin="8"
Symbol="Alert" />
<TextBlock
Foreground="{DynamicResource ThemeRedColor}"
Margin="0,8,8,8"
TextAlignment="Left"
TextWrapping="Wrap">
<Run Text="An installation with this name already exists." />
<LineBreak />
<Run Text="Please choose a different name or select a different Install Location." />
</TextBlock>
</StackPanel>
<TextBox
Margin="0,0,0,8"
Text="{Binding InstallName, Mode=TwoWay}" />
<!-- Install Button -->
<UniformGrid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
Columns="2"
Margin="0,32,0,0"
HorizontalAlignment="Center">
<Button
Content="Cancel"
Command="{Binding Cancel}"
FontSize="20"
HorizontalAlignment="Center"
Margin="8,0,4,0"
Padding="16, 8, 16, 8" />
<Button
Content="Install"
Command="{Binding InstallCommand}"
FontSize="20"
IsEnabled="{Binding CanInstall}"
HorizontalAlignment="Center"
Classes="success"
Margin="4,0,8,0"
Padding="16, 8, 16, 8" />
</UniformGrid>
<StackPanel Orientation="Horizontal">
<Button
Command="{Binding InstallCommand}"
Content="Install"
Classes="success"
Height="50"
IsEnabled="{Binding !ShowDuplicateWarning}"
Margin="0,16,0,0"
VerticalAlignment="Top"
Width="100" />
<controls:ProgressRing
Height="25"
IsIndeterminate="True"
BorderThickness="4"
Margin="8,16,0,0"
VerticalAlignment="Center"
IsVisible="{Binding InstallProgress.IsProgressVisible}"
Width="25" />
<TextBlock
Margin="8,16,0,0"
Text="Installing..."
VerticalAlignment="Center"
IsVisible="{Binding InstallProgress.IsProgressVisible}" />
</StackPanel>
</StackPanel>
<ScrollViewer Grid.Column="2"
MaxWidth="300"
Margin="16"
MaxHeight="600">
<mdxaml:MarkdownScrollViewer
Markdown="{Binding ReleaseNotes, Mode=OneWay}"/>
</ScrollViewer>
</Grid>
<!-- Available Packages -->
<ListBox Grid.Column="1" Grid.Row="0"
Margin="8, 16"
ItemsSource="{Binding AvailablePackages}"
SelectedItem="{Binding SelectedPackage}">
<ListBox.Template>
<ControlTemplate>
<ItemsPresenter />
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type packages:BasePackage}">
<StackPanel Margin="8">
<TextBlock Text="{Binding DisplayName}" />
<TextBlock Text="{Binding ByAuthor}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</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);
});
};
}
}

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

@ -14,7 +14,7 @@ namespace StabilityMatrix.Avalonia.Views;
public partial class LaunchPageView : UserControlBase
{
private const int LineOffset = 5;
public LaunchPageView()
{
InitializeComponent();
@ -22,17 +22,18 @@ public partial class LaunchPageView : UserControlBase
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");
if (scope is null)
throw new InvalidOperationException("Scope is null");
textMate.SetGrammar(scope);
textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus));
}
@ -55,7 +56,8 @@ public partial class LaunchPageView : UserControlBase
Dispatcher.UIThread.Invoke(() =>
{
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);
editor.ScrollToLine(line);
});

2
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -118,7 +118,7 @@
<controls:BetterFlyout Placement="RightEdgeAlignedBottom"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Auto"
Content="{Binding }" />
Content="{Binding }"/>
</ui:NavigationViewItem.ContextFlyout>
<ui:NavigationViewItem.InfoBadge>
<ui:InfoBadge Value="{Binding ProgressItems.Count}"

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

@ -25,6 +25,7 @@ using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Processes;
#if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.Views;
@ -38,6 +39,8 @@ public partial class MainWindow : AppWindowBase
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
private FlyoutBase? progressFlyout;
[DesignOnly(true)]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public MainWindow()
@ -62,6 +65,8 @@ public partial class MainWindow : AppWindowBase
#endif
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
EventManager.Instance.ToggleProgressFlyout += (_, _) => progressFlyout?.Hide();
}
/// <inheritdoc />
@ -231,6 +236,8 @@ public partial class MainWindow : AppWindowBase
var item = sender as NavigationViewItem;
var flyout = item!.ContextFlyout;
flyout!.ShowAt(item);
progressFlyout = flyout;
}
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:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmBase="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base"
xmlns:progress="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Progress"
MaxHeight="250"
d:DataContext="{x:Static mocks:DesignData.ProgressManagerViewModel}"
d:DesignHeight="250"
d:DesignWidth="300"
x:DataType="vm:ProgressManagerViewModel"
x:DataType="progress:ProgressManagerViewModel"
mc:Ignorable="d">
<ScrollViewer>
<Grid RowDefinitions="Auto, *">
@ -117,7 +118,7 @@
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ProgressItemViewModel}">
<DataTemplate DataType="{x:Type progress:ProgressItemViewModel}">
<Border
Margin="4"
Padding="8"
@ -164,6 +165,70 @@
</Grid>
</Border>
</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>
</Grid>

3
StabilityMatrix.Core/Database/ILiteDbContext.cs

@ -7,13 +7,12 @@ namespace StabilityMatrix.Core.Database;
public interface ILiteDbContext : IDisposable
{
LiteDatabaseAsync Database { get; }
ILiteCollectionAsync<CivitModel> CivitModels { get; }
ILiteCollectionAsync<CivitModelVersion> CivitModelVersions { get; }
ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache { get; }
ILiteCollectionAsync<LocalModelFile> LocalModelFiles { get; }
Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3);
Task<bool> UpsertCivitModelAsync(CivitModel civitModel);
Task<bool> UpsertCivitModelAsync(IEnumerable<CivitModel> civitModels);

115
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -21,18 +21,24 @@ public class LiteDbContext : ILiteDbContext
// Notification events
public event EventHandler? CivitModelsChanged;
// Collections (Tables)
public ILiteCollectionAsync<CivitModel> CivitModels => Database.GetCollection<CivitModel>("CivitModels");
public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions => Database.GetCollection<CivitModelVersion>("CivitModelVersions");
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 ILiteCollectionAsync<CivitModel> CivitModels =>
Database.GetCollection<CivitModel>("CivitModels");
public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions =>
Database.GetCollection<CivitModelVersion>("CivitModelVersions");
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(
ILogger<LiteDbContext> logger,
ISettingsManager settingsManager,
IOptions<DebugOptions> debugOptions)
ISettingsManager settingsManager,
IOptions<DebugOptions> debugOptions
)
{
this.logger = logger;
this.settingsManager = settingsManager;
@ -42,7 +48,7 @@ public class LiteDbContext : ILiteDbContext
private LiteDatabaseAsync CreateDatabase()
{
LiteDatabaseAsync? db = null;
if (debugOptions.TempDatabase)
{
db = new LiteDatabaseAsync(":temp:");
@ -53,54 +59,74 @@ public class LiteDbContext : ILiteDbContext
try
{
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync(new ConnectionString()
{
Filename = dbPath,
Connection = ConnectionType.Shared,
});
db = new LiteDatabaseAsync(
new ConnectionString()
{
Filename = dbPath,
Connection = ConnectionType.Shared,
}
);
}
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
);
}
}
// Fallback to temporary database
db ??= new LiteDatabaseAsync(":temp:");
// Register reference fields
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(m => m.ModelVersions, "CivitModelVersions");
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(e => e.Items, "CivitModels");
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(
m => m.ModelVersions,
"CivitModelVersions"
);
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(
e => e.Items,
"CivitModels"
);
return db;
}
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3)
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(
string hashBlake3
)
{
var version = await CivitModelVersions.Query()
.Where(mv => mv.Files!
.Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3))
var version = await CivitModelVersions
.Query()
.Where(
mv =>
mv.Files!
.Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3)
)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
if (version is null) return (null, null);
var model = await CivitModels.Query()
if (version is null)
return (null, null);
var model = await CivitModels
.Query()
.Include(m => m.ModelVersions)
.Where(m => m.ModelVersions!
.Select(v => v.Id)
.Any(id => id == version.Id))
.FirstOrDefaultAsync().ConfigureAwait(false);
.Where(m => m.ModelVersions!.Select(v => v.Id).Any(id => id == version.Id))
.FirstOrDefaultAsync()
.ConfigureAwait(false);
return (model, version);
}
public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel)
{
// 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);
// Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated;
@ -110,7 +136,7 @@ public class LiteDbContext : ILiteDbContext
}
return anyUpdated;
}
public async Task<bool> UpsertCivitModelAsync(IEnumerable<CivitModel> civitModels)
{
var civitModelsArray = civitModels.ToArray();
@ -126,7 +152,7 @@ public class LiteDbContext : ILiteDbContext
}
return anyUpdated;
}
// Add to cache
public async Task<bool> UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry)
{
@ -141,13 +167,14 @@ public class LiteDbContext : ILiteDbContext
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)
{
return result;
}
return null;
}
@ -162,9 +189,7 @@ public class LiteDbContext : ILiteDbContext
{
database.Dispose();
}
catch (ObjectDisposedException)
{
}
catch (ObjectDisposedException) { }
database = null;
}

44
StabilityMatrix.Core/Helper/EventManager.cs

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

81
StabilityMatrix.Core/Helper/SharedFolders.cs

@ -20,7 +20,7 @@ public class SharedFolders : ISharedFolders
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
}
// Platform redirect for junctions / symlinks
private static void CreateLinkOrJunction(string junctionDir, string targetDir, bool overwrite)
{
@ -36,8 +36,11 @@ public class SharedFolders : ISharedFolders
}
}
public static void SetupLinks(Dictionary<SharedFolderType, IReadOnlyList<string>> definitions,
DirectoryPath modelsDirectory, DirectoryPath installDirectory)
public static void SetupLinks(
Dictionary<SharedFolderType, IReadOnlyList<string>> definitions,
DirectoryPath modelsDirectory,
DirectoryPath installDirectory
)
{
foreach (var (folderType, relativePaths) in definitions)
{
@ -63,7 +66,9 @@ public class SharedFolders : ISharedFolders
// Skip name collisions
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;
}
destinationFile.Info.MoveTo(sourceFile);
@ -81,19 +86,25 @@ public class SharedFolders : ISharedFolders
{
var modelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var sharedFolders = basePackage.SharedFolders;
if (sharedFolders == null) return;
if (sharedFolders == null)
return;
SetupLinks(sharedFolders, modelsDirectory, installDirectory);
}
/// <summary>
/// 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.
/// </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;
if (sharedFolders is null) return;
if (sharedFolders is null)
return;
foreach (var (folderType, relativePaths) in sharedFolders)
{
foreach (var relativePath in relativePaths)
@ -117,7 +128,8 @@ public class SharedFolders : ISharedFolders
if (destinationDir.Info.LinkTarget == sourceDir)
{
Logger.Info(
$"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})");
$"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})"
);
return;
}
@ -131,8 +143,12 @@ public class SharedFolders : ISharedFolders
if (destinationDir.Info.EnumerateFileSystemInfos().Any())
{
Logger.Info($"Moving files from {destinationDir} to {sourceDir}");
await FileTransfers.MoveAllFilesAndDirectories(
destinationDir, sourceDir, overwriteIfHashMatches: true)
await FileTransfers
.MoveAllFilesAndDirectories(
destinationDir,
sourceDir,
overwriteIfHashMatches: true
)
.ConfigureAwait(false);
}
@ -154,15 +170,16 @@ public class SharedFolders : ISharedFolders
{
return;
}
foreach (var (_, relativePaths) in sharedFolders)
{
foreach (var relativePath in relativePaths)
{
var destination = Path.GetFullPath(Path.Combine(installPath, relativePath));
// Delete the destination folder if it exists
if (!Directory.Exists(destination)) continue;
if (!Directory.Exists(destination))
continue;
Logger.Info($"Deleting junction target {destination}");
Directory.Delete(destination, false);
}
@ -174,19 +191,32 @@ public class SharedFolders : ISharedFolders
var packages = settingsManager.Settings.InstalledPackages;
foreach (var package in packages)
{
if (package.PackageName == null) continue;
if (package.PackageName == null)
continue;
var basePackage = packageFactory[package.PackageName];
if (basePackage == null) continue;
if (package.LibraryPath == null) continue;
if (basePackage == null)
continue;
if (package.LibraryPath == null)
continue;
try
{
basePackage.RemoveModelFolderLinks(package.FullPath).GetAwaiter().GetResult();
var sharedFolderMethod =
package.PreferredSharedFolderMethod
?? basePackage.RecommendedSharedFolderMethod;
basePackage
.RemoveModelFolderLinks(package.FullPath, sharedFolderMethod)
.GetAwaiter()
.GetResult();
}
catch (Exception e)
{
Logger.Warn("Failed to remove links for package {Package} " +
"({DisplayName}): {Message}", package.PackageName, package.DisplayName, e.Message);
Logger.Warn(
"Failed to remove links for package {Package} " + "({DisplayName}): {Message}",
package.PackageName,
package.DisplayName,
e.Message
);
}
}
}
@ -194,8 +224,9 @@ public class SharedFolders : ISharedFolders
public void SetupSharedModelFolders()
{
var modelsDir = settingsManager.ModelsDirectory;
if (string.IsNullOrWhiteSpace(modelsDir)) return;
if (string.IsNullOrWhiteSpace(modelsDir))
return;
Directory.CreateDirectory(modelsDir);
var allSharedFolderTypes = Enum.GetValues<SharedFolderType>();
foreach (var sharedFolder in allSharedFolderTypes)

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 string? Sha { get; set; }
[JsonIgnore]
public string ShortSha => string.IsNullOrWhiteSpace(Sha) ? string.Empty : Sha[..7];
}

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

@ -22,7 +22,7 @@ public class LocalModelFile
/// Optional connected model information.
/// </summary>
public ConnectedModelInfo? ConnectedModelInfo { get; set; }
/// <summary>
/// Optional preview image relative path.
/// </summary>
@ -32,10 +32,11 @@ public class LocalModelFile
{
return Path.Combine(rootModelDirectory, RelativePath);
}
public string? GetPreviewImageFullPath(string rootModelDirectory)
{
return PreviewImageRelativePath == null ? null
return PreviewImageRelativePath == null
? null
: 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; }
}

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

@ -10,6 +10,7 @@ namespace StabilityMatrix.Core.Models.FileInterfaces;
public class FilePath : FileSystemPath, IPathObject
{
private FileInfo? _info;
// ReSharper disable once MemberCanBePrivate.Global
[JsonIgnore]
public FileInfo Info => _info ??= new FileInfo(FullPath);
@ -23,17 +24,16 @@ public class FilePath : FileSystemPath, IPathObject
return Info.Attributes.HasFlag(FileAttributes.ReparsePoint);
}
}
[JsonIgnore]
public bool Exists => Info.Exists;
[JsonIgnore]
public string Name => Info.Name;
[JsonIgnore]
public string NameWithoutExtension
=> Path.GetFileNameWithoutExtension(Info.Name);
[JsonIgnore]
public string NameWithoutExtension => Path.GetFileNameWithoutExtension(Info.Name);
/// <summary>
/// Get the directory of the file.
/// </summary>
@ -44,8 +44,7 @@ public class FilePath : FileSystemPath, IPathObject
{
try
{
return Info.Directory == null ? null
: new DirectoryPath(Info.Directory);
return Info.Directory == null ? null : new DirectoryPath(Info.Directory);
}
catch (DirectoryNotFoundException)
{
@ -54,32 +53,31 @@ public class FilePath : FileSystemPath, IPathObject
}
}
public FilePath(string path) : base(path)
{
}
public FilePath(FileInfo fileInfo) : base(fileInfo.FullName)
public FilePath(string path)
: base(path) { }
public FilePath(FileInfo fileInfo)
: base(fileInfo.FullName)
{
_info = fileInfo;
}
public FilePath(FileSystemPath path) : base(path)
{
}
public FilePath(params string[] paths) : base(paths)
{
}
public FilePath(FileSystemPath path)
: base(path) { }
public FilePath(params string[] paths)
: base(paths) { }
public long GetSize()
{
Info.Refresh();
return Info.Length;
}
public long GetSize(bool includeSymbolicLinks)
{
if (!includeSymbolicLinks && IsSymbolicLink) return 0;
if (!includeSymbolicLinks && IsSymbolicLink)
return 0;
return GetSize();
}
@ -87,51 +85,51 @@ public class FilePath : FileSystemPath, IPathObject
{
return Task.Run(() => GetSize(includeSymbolicLinks));
}
/// <summary> Creates an empty file. </summary>
public void Create() => File.Create(FullPath).Close();
/// <summary> Deletes the file </summary>
public void Delete() => File.Delete(FullPath);
// Methods specific to files
/// <summary> Read text </summary>
public string ReadAllText() => File.ReadAllText(FullPath);
/// <summary> Read text asynchronously </summary>
public Task<string> ReadAllTextAsync(CancellationToken ct = default)
{
return File.ReadAllTextAsync(FullPath, ct);
}
/// <summary> Write text </summary>
public void WriteAllText(string text) => File.WriteAllText(FullPath, text, Encoding.UTF8);
/// <summary> Write text asynchronously </summary>
public Task WriteAllTextAsync(string text, CancellationToken ct = default)
{
return File.WriteAllTextAsync(FullPath, text, Encoding.UTF8, ct);
}
/// <summary> Read bytes </summary>
public byte[] ReadAllBytes() => File.ReadAllBytes(FullPath);
/// <summary> Read bytes asynchronously </summary>
public Task<byte[]> ReadAllBytesAsync(CancellationToken ct = default)
{
return File.ReadAllBytesAsync(FullPath, ct);
}
/// <summary> Write bytes </summary>
public void WriteAllBytes(byte[] bytes) => File.WriteAllBytes(FullPath, bytes);
/// <summary> Write bytes asynchronously </summary>
public Task WriteAllBytesAsync(byte[] bytes, CancellationToken ct = default)
{
return File.WriteAllBytesAsync(FullPath, bytes, ct);
}
/// <summary>
/// Move the file to a directory.
/// </summary>
@ -141,7 +139,7 @@ public class FilePath : FileSystemPath, IPathObject
// Return the new path
return destinationFile;
}
/// <summary>
/// Move the file to a directory.
/// </summary>
@ -151,7 +149,7 @@ public class FilePath : FileSystemPath, IPathObject
// Return the new path
return directory.JoinFile(this);
}
/// <summary>
/// Move the file to a target path.
/// </summary>
@ -161,7 +159,7 @@ public class FilePath : FileSystemPath, IPathObject
// Return the new path
return destinationFile;
}
/// <summary>
/// Copy the file to a target path.
/// </summary>
@ -174,5 +172,6 @@ public class FilePath : FileSystemPath, IPathObject
// Implicit conversions to and from string
public static implicit operator string(FilePath path) => path.FullPath;
public static implicit operator FilePath(string path) => new(path);
}

110
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -6,40 +6,52 @@ namespace StabilityMatrix.Core.Models;
/// <summary>
/// Profile information for a user-installed package.
/// </summary>
public class InstalledPackage
public class InstalledPackage : IJsonOnDeserialized
{
// Unique ID for the installation
public Guid Id { get; set; }
// User defined name
public string? DisplayName { get; set; }
// Package name
public string? PackageName { get; set; }
// Package version
[Obsolete("Use Version instead. (Kept for migration)")]
public string? PackageVersion { get; set; }
[Obsolete("Use Version instead. (Kept for migration)")]
public string? InstalledBranch { get; set; }
[Obsolete("Use Version instead. (Kept for migration)")]
public string? DisplayVersion { get; set; }
public InstalledPackageVersion? Version { get; set; }
// Old type absolute path
[Obsolete("Use LibraryPath instead. (Kept for migration)")]
public string? Path { get; set; }
/// <summary>
/// Relative path from the library root.
/// </summary>
public string? LibraryPath { get; set; }
/// <summary>
/// Full path to the package, using LibraryPath and GlobalConfig.LibraryDir.
/// </summary>
[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 List<LaunchOption>? LaunchArgs { get; set; }
public DateTimeOffset? LastUpdateCheck { get; set; }
public bool UpdateAvailable { get; set; }
public TorchVersion? PreferredTorchVersion { get; set; }
public SharedFolderMethod? PreferredSharedFolderMethod { get; set; }
/// <summary>
/// Get the path as a relative sub-path of the relative path.
/// If not a sub-path, return null.
@ -48,14 +60,16 @@ public class InstalledPackage
{
var relativePath = System.IO.Path.GetRelativePath(relativeTo, path);
// 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
var isSubPath = relativePath != "."
&& relativePath != ".."
&& !relativePath.StartsWith(".." + System.IO.Path.DirectorySeparatorChar)
&& !System.IO.Path.IsPathRooted(relativePath);
var isSubPath =
relativePath != "."
&& relativePath != ".."
&& !relativePath.StartsWith(".." + System.IO.Path.DirectorySeparatorChar)
&& !System.IO.Path.IsPathRooted(relativePath);
return isSubPath ? relativePath : null;
}
}
/// <summary>
/// Migrates the old Path to the new LibraryPath.
@ -67,12 +81,13 @@ public class InstalledPackage
#pragma warning disable CS0618
var oldPath = Path;
#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
var library = libraryDirectory ?? GlobalConfig.LibraryDir;
var relativePath = GetSubPath(library, oldPath);
// If so we migrate without any IO operations
if (relativePath != null)
{
@ -96,8 +111,9 @@ public class InstalledPackage
#pragma warning disable CS0618
var oldPath = Path;
#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
var library = libraryDirectory ?? GlobalConfig.LibraryDir;
var relativePath = GetSubPath(library, oldPath);
@ -114,7 +130,8 @@ public class InstalledPackage
#pragma warning disable CS0618
var oldPath = Path;
#pragma warning restore CS0618
if (oldPath == null) return;
if (oldPath == null)
return;
var libDir = libraryDirectory ?? GlobalConfig.LibraryDir;
// if old package Path is same as new library, return
@ -127,27 +144,31 @@ public class InstalledPackage
LibraryPath = System.IO.Path.Combine("Packages", DisplayName);
return;
}
// Try using pure migration first
if (TryPureMigratePath(libraryDirectory)) return;
if (TryPureMigratePath(libraryDirectory))
return;
// If not, we need to move the package directory
var packageFolderName = new DirectoryInfo(oldPath).Name;
// Get the new Library/Packages path
var library = libraryDirectory ?? GlobalConfig.LibraryDir;
var newPackagesDir = System.IO.Path.Combine(library, "Packages");
// Get the new target path
var newPackagePath = System.IO.Path.Combine(newPackagesDir, packageFolderName);
// Ensure it is not already there, if so, add a suffix until it's not
var suffix = 2;
while (Directory.Exists(newPackagePath))
{
newPackagePath = System.IO.Path.Combine(newPackagesDir, $"{packageFolderName}-{suffix}");
newPackagePath = System.IO.Path.Combine(
newPackagesDir,
$"{packageFolderName}-{suffix}"
);
suffix++;
}
// Move the package directory
await Task.Run(() => Utilities.CopyDirectory(oldPath, newPackagePath, true));
@ -160,7 +181,7 @@ public class InstalledPackage
public static IEqualityComparer<InstalledPackage> Comparer { get; } =
new PropertyComparer<InstalledPackage>(p => p.Id);
protected bool Equals(InstalledPackage other)
{
return Id.Equals(other.Id);
@ -168,13 +189,40 @@ public class InstalledPackage
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == this.GetType() && Equals((InstalledPackage) obj);
if (ReferenceEquals(null, obj))
return false;
if (ReferenceEquals(this, obj))
return true;
return obj.GetType() == this.GetType() && Equals((InstalledPackage)obj);
}
public override int 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 string? ReleaseNotesMarkdown { get; set; }
public bool IsPrerelease { get; set; }
}

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

@ -14,12 +14,12 @@ namespace StabilityMatrix.Core.Models.Packages;
public class A3WebUI : BaseGitPackage
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public override string Name => "stable-diffusion-webui";
public override string DisplayName { get; set; } = "Stable Diffusion WebUI";
public override string Author => "AUTOMATIC1111";
public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl =>
public override string LicenseUrl =>
"https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/LICENSE.txt";
public override string Blurb =>
"A browser interface based on Gradio library for Stable Diffusion";
@ -28,182 +28,201 @@ public class A3WebUI : BaseGitPackage
new("https://github.com/AUTOMATIC1111/stable-diffusion-webui/raw/master/screenshot.png");
public string RelativeArgsDefinitionScriptPath => "modules.cmd_args";
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public A3WebUI(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) :
base(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
}
public A3WebUI(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
// From https://github.com/AUTOMATIC1111/stable-diffusion-webui/tree/master/models
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
{
[SharedFolderType.StableDiffusion] = new[] {"models/Stable-diffusion"},
[SharedFolderType.ESRGAN] = new[] {"models/ESRGAN"},
[SharedFolderType.RealESRGAN] = new[] {"models/RealESRGAN"},
[SharedFolderType.SwinIR] = new[] {"models/SwinIR"},
[SharedFolderType.Lora] = new[] {"models/Lora"},
[SharedFolderType.LyCORIS] = new[] {"models/LyCORIS"},
[SharedFolderType.ApproxVAE] = new[] {"models/VAE-approx"},
[SharedFolderType.VAE] = new[] {"models/VAE"},
[SharedFolderType.DeepDanbooru] = new[] {"models/deepbooru"},
[SharedFolderType.Karlo] = new[] {"models/karlo"},
[SharedFolderType.TextualInversion] = new[] {"embeddings"},
[SharedFolderType.Hypernetwork] = new[] {"models/hypernetworks"},
[SharedFolderType.ControlNet] = new[] {"models/ControlNet"}
};
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions => new()
{
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new() {"--host"}
},
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "7860",
Options = new() {"--port"}
},
[SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" },
[SharedFolderType.ESRGAN] = new[] { "models/ESRGAN" },
[SharedFolderType.RealESRGAN] = new[] { "models/RealESRGAN" },
[SharedFolderType.SwinIR] = new[] { "models/SwinIR" },
[SharedFolderType.Lora] = new[] { "models/Lora" },
[SharedFolderType.LyCORIS] = new[] { "models/LyCORIS" },
[SharedFolderType.ApproxVAE] = new[] { "models/VAE-approx" },
[SharedFolderType.VAE] = new[] { "models/VAE" },
[SharedFolderType.DeepDanbooru] = new[] { "models/deepbooru" },
[SharedFolderType.Karlo] = new[] { "models/karlo" },
[SharedFolderType.TextualInversion] = new[] { "embeddings" },
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" },
[SharedFolderType.ControlNet] = new[] { "models/ControlNet" }
};
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
new()
{
Level.Low => "--lowvram",
Level.Medium => "--medvram",
_ => null
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new() { "--host" }
},
Options = new() { "--lowvram", "--medvram" }
},
new()
{
Name = "Xformers",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.HasNvidiaGpu(),
Options = new() { "--xformers" }
},
new()
{
Name = "API",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = new() {"--api"}
},
new()
{
Name = "Skip Torch CUDA Check",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = new() {"--skip-torch-cuda-test"}
},
new()
{
Name = "Skip Python Version Check",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = new() {"--skip-python-version-check"}
},
new()
{
Name = "No Half",
Type = LaunchOptionType.Bool,
Description = "Do not switch the model to 16-bit floats",
InitialValue = HardwareHelper.HasAmdGpu(),
Options = new() {"--no-half"}
},
LaunchOptionDefinition.Extras
};
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "7860",
Options = new() { "--port" }
},
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{
Level.Low => "--lowvram",
Level.Medium => "--medvram",
_ => null
},
Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" }
},
new()
{
Name = "Xformers",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.HasNvidiaGpu(),
Options = new() { "--xformers" }
},
new()
{
Name = "API",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = new() { "--api" }
},
new()
{
Name = "Skip Torch CUDA Check",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = new() { "--skip-torch-cuda-test" }
},
new()
{
Name = "Skip Python Version Check",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = new() { "--skip-python-version-check" }
},
new()
{
Name = "No Half",
Type = LaunchOptionType.Bool,
Description = "Do not switch the model to 16-bit floats",
InitialValue = HardwareHelper.HasAmdGpu(),
Options = new() { "--no-half" }
},
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()
{
var release = await GetLatestRelease().ConfigureAwait(false);
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));
// Setup venv
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation;
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false);
// Install torch / xformers based on gpu info
var gpus = HardwareHelper.IterGpuInfo().ToList();
if (gpus.Any(g => g.IsNvidia))
switch (torchVersion)
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true));
Logger.Info("Starting torch install (CUDA)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput)
.ConfigureAwait(false);
Logger.Info("Installing xformers...");
await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false);
}
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);
}
else
{
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);
case TorchVersion.Cpu:
await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
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);
break;
default:
throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null);
}
// 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");
await venvRunner.PipInstall($"-r requirements_versions.txt", OnConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Installing Package Requirements", isIndeterminate: 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, "Updating configuration", isIndeterminate: true));
// Create and add {"show_progress_type": "TAESD"} to config.json
// 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))
{
var config = new JsonObject {{"show_progress_type", "TAESD"}};
var config = new JsonObject { { "show_progress_type", "TAESD" } };
await File.WriteAllTextAsync(configPath, config.ToString()).ConfigureAwait(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);
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;
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
var match = regex.Match(s.Text);
if (!match.Success)
return;
WebUrl = match.Value;
OnStartupComplete(WebUrl);
}
@ -212,4 +231,21 @@ public class A3WebUI : BaseGitPackage
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);
}
}

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

@ -7,6 +7,7 @@ using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
@ -21,13 +22,13 @@ namespace StabilityMatrix.Core.Models.Packages;
public abstract class BaseGitPackage : BasePackage
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
protected readonly IGithubApiCache GithubApi;
protected readonly ISettingsManager SettingsManager;
protected readonly IDownloadService DownloadService;
protected readonly IPrerequisiteHelper PrerequisiteHelper;
public PyVenvRunner? VenvRunner;
/// <summary>
/// URL of the hosted web page on launch
/// </summary>
@ -35,20 +36,35 @@ public abstract class BaseGitPackage : BasePackage
public override string GithubUrl => $"https://github.com/{Author}/{Name}";
public override string DownloadLocation =>
public string DownloadLocation =>
Path.Combine(SettingsManager.LibraryDir, "Packages", $"{Name}.zip");
public override string InstallLocation { get; set; }
protected string GetDownloadUrl(string tagName, bool isCommitHash = false)
protected string GetDownloadUrl(DownloadPackageVersionOptions versionOptions)
{
return isCommitHash
? $"https://github.com/{Author}/{Name}/archive/{tagName}.zip"
: $"https://api.github.com/repos/{Author}/{Name}/zipball/{tagName}";
if (!string.IsNullOrWhiteSpace(versionOptions.CommitHash))
{
return $"https://github.com/{Author}/{Name}/archive/{versionOptions.CommitHash}.zip";
}
if (!string.IsNullOrWhiteSpace(versionOptions.VersionTag))
{
return $"https://api.github.com/repos/{Author}/{Name}/zipball/{versionOptions.VersionTag}";
}
if (!string.IsNullOrWhiteSpace(versionOptions.BranchName))
{
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)
protected BaseGitPackage(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
{
GithubApi = githubApi;
SettingsManager = settingsManager;
@ -58,48 +74,55 @@ public abstract class BaseGitPackage : BasePackage
protected async Task<Release> GetLatestRelease(bool includePrerelease = false)
{
var releases = await GithubApi
.GetAllReleases(Author, Name)
.ConfigureAwait(false);
var releases = await GithubApi.GetAllReleases(Author, Name).ConfigureAwait(false);
return includePrerelease ? releases.First() : releases.First(x => !x.Prerelease);
}
public override Task<IEnumerable<Branch>> GetAllBranches()
{
return GithubApi.GetAllBranches(Author, Name);
}
public override Task<IEnumerable<Release>> GetAllReleases()
{
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);
}
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 releasesList = allReleases.ToList();
if (releasesList.Any())
{
var allReleases = await GetAllReleases().ConfigureAwait(false);
return allReleases.Where(r => r.Prerelease == false).Select(r =>
new PackageVersion
{
TagName = r.TagName!,
ReleaseNotesMarkdown = r.Body
});
packageVersionOptions.AvailableVersions = releasesList.Select(
r =>
new PackageVersion
{
TagName = r.TagName!,
ReleaseNotesMarkdown = r.Body,
IsPrerelease = r.Prerelease
}
);
}
// Branch mode
var allBranches = await GetAllBranches().ConfigureAwait(false);
return allBranches.Select(b => new PackageVersion
{
TagName = $"{b.Name}",
ReleaseNotesMarkdown = string.Empty
});
packageVersionOptions.AvailableBranches = allBranches.Select(
b => new PackageVersion { TagName = $"{b.Name}", ReleaseNotesMarkdown = string.Empty }
);
return packageVersionOptions;
}
/// <summary>
@ -107,9 +130,10 @@ public abstract class BaseGitPackage : BasePackage
/// </summary>
[MemberNotNull(nameof(VenvRunner))]
public async Task<PyVenvRunner> SetupVenv(
string installedPackagePath,
string installedPackagePath,
string venvName = "venv",
bool forceRecreate = false)
bool forceRecreate = false
)
{
var venvPath = Path.Combine(installedPackagePath, venvName);
if (VenvRunner != null)
@ -122,26 +146,27 @@ public abstract class BaseGitPackage : BasePackage
WorkingDirectory = installedPackagePath,
EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables,
};
if (!VenvRunner.Exists() || forceRecreate)
{
await VenvRunner.Setup(forceRecreate).ConfigureAwait(false);
}
return VenvRunner;
}
public override async Task<IEnumerable<Release>> GetReleaseTags()
{
var allReleases = await GithubApi
.GetAllReleases(Author, Name)
.ConfigureAwait(false);
var allReleases = await GithubApi.GetAllReleases(Author, Name).ConfigureAwait(false);
return allReleases;
}
public override async Task<string> DownloadPackage(string version, bool isCommitHash,
string? branch, IProgress<ProgressReport>? progress = null)
public override async Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null
)
{
var downloadUrl = GetDownloadUrl(version, isCommitHash);
var downloadUrl = GetDownloadUrl(versionOptions);
if (!Directory.Exists(DownloadLocation.Replace($"{Name}.zip", "")))
{
@ -153,24 +178,26 @@ public abstract class BaseGitPackage : BasePackage
.ConfigureAwait(false);
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);
progress?.Report(new ProgressReport(1f, $"{DisplayName} installed successfully"));
await UnzipPackage(installLocation, progress).ConfigureAwait(false);
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);
var zipDirName = string.Empty;
var totalEntries = zip.Entries.Count;
var currentEntry = 0;
foreach (var entry in zip.Entries)
{
currentEntry++;
@ -180,56 +207,58 @@ public abstract class BaseGitPackage : BasePackage
{
zipDirName = entry.FullName;
}
var folderPath = Path.Combine(InstallLocation,
entry.FullName.Replace(zipDirName, string.Empty));
var folderPath = Path.Combine(
installLocation,
entry.FullName.Replace(zipDirName, string.Empty)
);
Directory.CreateDirectory(folderPath);
continue;
}
var destinationPath = Path.GetFullPath(Path.Combine(InstallLocation,
entry.FullName.Replace(zipDirName, string.Empty)));
var destinationPath = Path.GetFullPath(
Path.Combine(installLocation, entry.FullName.Replace(zipDirName, string.Empty))
);
entry.ExtractToFile(destinationPath, true);
progress?.Report(new ProgressReport(current: Convert.ToUInt64(currentEntry),
total: Convert.ToUInt64(totalEntries)));
progress?.Report(
new ProgressReport(
current: Convert.ToUInt64(currentEntry),
total: Convert.ToUInt64(totalEntries)
)
);
}
progress?.Report(new ProgressReport(1f, $"{DisplayName} installed successfully"));
return Task.CompletedTask;
}
public override async Task<bool> CheckForUpdates(InstalledPackage package)
{
var currentVersion = package.PackageVersion;
if (string.IsNullOrWhiteSpace(currentVersion))
var currentVersion = package.Version;
if (currentVersion == null)
{
return false;
}
try
{
if (string.IsNullOrWhiteSpace(package.InstalledBranch))
if (currentVersion.IsReleaseMode)
{
var latestVersion = await GetLatestVersion().ConfigureAwait(false);
UpdateAvailable = latestVersion != currentVersion;
return latestVersion != currentVersion;
UpdateAvailable = latestVersion != currentVersion.InstalledReleaseVersion;
return UpdateAvailable;
}
else
var allCommits = (
await GetAllCommits(currentVersion.InstalledBranch).ConfigureAwait(false)
)?.ToList();
if (allCommits == null || !allCommits.Any())
{
var allCommits = (await GetAllCommits(package.InstalledBranch)
.ConfigureAwait(false))?.ToList();
if (allCommits == null || !allCommits.Any())
{
Logger.Warn("No commits found for {Package}", package.PackageName);
return false;
}
var latestCommitHash = allCommits.First().Sha;
return latestCommitHash != currentVersion;
Logger.Warn("No commits found for {Package}", package.PackageName);
return false;
}
var latestCommitHash = allCommits.First().Sha;
return latestCommitHash != currentVersion.InstalledCommitSha;
}
catch (ApiException e)
{
@ -238,23 +267,38 @@ public abstract class BaseGitPackage : BasePackage
}
}
public override async Task<string> Update(InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null, bool includePrerelease = false)
public override async Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
)
{
// Release mode
if (string.IsNullOrWhiteSpace(installedPackage.InstalledBranch))
if (installedPackage.Version == null)
throw new NullReferenceException("Version is null");
if (installedPackage.Version.IsReleaseMode)
{
var releases = await GetAllReleases().ConfigureAwait(false);
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);
await InstallPackage(progress).ConfigureAwait(false);
return latestRelease.TagName;
return new InstalledPackageVersion { InstalledReleaseVersion = latestRelease.TagName };
}
// Commit mode
var allCommits = await GetAllCommits(
installedPackage.InstalledBranch).ConfigureAwait(false);
var allCommits = await GetAllCommits(installedPackage.Version.InstalledBranch)
.ConfigureAwait(false);
var latestCommit = allCommits?.First();
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");
}
await DownloadPackage(latestCommit.Sha, true, installedPackage.InstalledBranch, progress)
await DownloadPackage(
installedPackage.FullPath,
new DownloadPackageVersionOptions { CommitHash = latestCommit.Sha },
progress
)
.ConfigureAwait(false);
await InstallPackage(installedPackage.FullPath, torchVersion, progress)
.ConfigureAwait(false);
await InstallPackage(progress).ConfigureAwait(false);
return latestCommit.Sha;
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
.SetupLinks(folders, SettingsManager.ModelsDirectory, installDirectory);
StabilityMatrix.Core.Helper.SharedFolders.SetupLinks(
folders,
SettingsManager.ModelsDirectory,
installDirectory
);
}
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,
SettingsManager.ModelsDirectory, installDirectory).ConfigureAwait(false);
await StabilityMatrix.Core.Helper.SharedFolders
.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);
}
@ -307,7 +375,7 @@ public abstract class BaseGitPackage : BasePackage
}
process.StandardInput.WriteLine(input);
}
public virtual async Task SendInputAsync(string input)
{
var process = VenvRunner?.Process;
@ -328,7 +396,7 @@ public abstract class BaseGitPackage : BasePackage
VenvRunner = null;
}
}
/// <inheritdoc />
public override async Task WaitForShutdown()
{

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

@ -1,8 +1,10 @@
using Octokit;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
namespace StabilityMatrix.Core.Models.Packages;
@ -19,29 +21,107 @@ public abstract class BasePackage
public abstract string LicenseUrl { get; }
public virtual string Disclaimer => string.Empty;
public virtual bool OfferInOneClickInstaller => true;
/// <summary>
/// Primary command to launch the package. 'Launch' buttons uses this.
/// </summary>
public abstract string LaunchCommand { get; }
/// <summary>
/// Optional commands (e.g. 'config') that are on the launch button split drop-down.
/// </summary>
public virtual IReadOnlyList<string> ExtraLaunchCommands { get; } = Array.Empty<string>();
public abstract Uri PreviewImageUri { get; }
public virtual bool ShouldIgnoreReleases => false;
public virtual bool UpdateAvailable { get; set; }
public abstract Task<string> DownloadPackage(string version, bool isCommitHash, string? branch,
IProgress<ProgressReport>? progress = null);
public abstract Task InstallPackage(IProgress<ProgressReport>? progress = null);
public abstract Task RunPackage(string installedPackagePath, string command, string arguments);
public abstract Task SetupModelFolders(DirectoryPath installDirectory);
public abstract Task UpdateModelFolders(DirectoryPath installDirectory);
public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory);
public abstract Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress1
);
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>
/// Shuts down the subprocess, canceling any pending streams.
/// </summary>
@ -51,36 +131,81 @@ public abstract class BasePackage
/// Shuts down the process, returning a Task to wait for output EOF.
/// </summary>
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 List<LaunchOptionDefinition> LaunchOptions { get; }
public virtual string? ExtraLaunchArguments { get; set; } = null;
/// <summary>
/// The shared folders that this package supports.
/// Mapping of <see cref="SharedFolderType"/> to the relative paths from the package root.
/// </summary>
public virtual Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Task<string> GetLatestVersion();
public abstract Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true);
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10);
public abstract Task<PackageVersionOptions> GetAllVersionOptions();
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(
string branch,
int page = 1,
int perPage = 10
);
public abstract Task<IEnumerable<Branch>> GetAllBranches();
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<string>? StartupComplete;
public void OnConsoleOutput(ProcessOutput output) => ConsoleOutput?.Invoke(this, output);
public void OnExit(int exitCode) => Exited?.Invoke(this, exitCode);
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);
}
}

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

@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Drawing;
using System.Text.RegularExpressions;
using NLog;
using StabilityMatrix.Core.Helper;
@ -20,7 +21,7 @@ public class ComfyUI : BaseGitPackage
public override string DisplayName { get; set; } = "ComfyUI";
public override string Author => "comfyanonymous";
public override string LicenseType => "GPL-3.0";
public override string LicenseUrl =>
public override string LicenseUrl =>
"https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE";
public override string Blurb => "A powerful and modular stable diffusion GUI and backend";
public override string LaunchCommand => "main.py";
@ -28,132 +29,164 @@ public class ComfyUI : BaseGitPackage
public override Uri PreviewImageUri =>
new("https://github.com/comfyanonymous/ComfyUI/raw/master/comfyui_screenshot.png");
public override bool ShouldIgnoreReleases => true;
public override SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Configuration;
public ComfyUI(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) :
base(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
}
public ComfyUI(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
// https://github.com/comfyanonymous/ComfyUI/blob/master/folder_paths.py#L11
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
{
[SharedFolderType.StableDiffusion] = new[] {"models/checkpoints"},
[SharedFolderType.Diffusers] = new[] {"models/diffusers"},
[SharedFolderType.Lora] = new[] {"models/loras"},
[SharedFolderType.CLIP] = new[] {"models/clip"},
[SharedFolderType.TextualInversion] = new[] {"models/embeddings"},
[SharedFolderType.VAE] = new[] {"models/vae"},
[SharedFolderType.ApproxVAE] = new[] {"models/vae_approx"},
[SharedFolderType.ControlNet] = new[] {"models/controlnet"},
[SharedFolderType.GLIGEN] = new[] {"models/gligen"},
[SharedFolderType.ESRGAN] = new[] {"models/upscale_models"},
[SharedFolderType.Hypernetwork] = new[] {"models/hypernetworks"},
};
public override List<LaunchOptionDefinition> LaunchOptions => new List<LaunchOptionDefinition>
{
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
[SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" },
[SharedFolderType.Diffusers] = new[] { "models/diffusers" },
[SharedFolderType.Lora] = new[] { "models/loras" },
[SharedFolderType.CLIP] = new[] { "models/clip" },
[SharedFolderType.TextualInversion] = new[] { "models/embeddings" },
[SharedFolderType.VAE] = new[] { "models/vae" },
[SharedFolderType.ApproxVAE] = new[] { "models/vae_approx" },
[SharedFolderType.ControlNet] = new[] { "models/controlnet" },
[SharedFolderType.GLIGEN] = new[] { "models/gligen" },
[SharedFolderType.ESRGAN] = new[] { "models/upscale_models" },
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" },
};
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{
new()
{
Level.Low => "--lowvram",
Level.Medium => "--normalvram",
_ => null
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{
Level.Low => "--lowvram",
Level.Medium => "--normalvram",
_ => null
},
Options = { "--highvram", "--normalvram", "--lowvram", "--novram" }
},
Options = { "--highvram", "--normalvram", "--lowvram", "--novram" }
},
new()
{
Name = "Use CPU only",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = {"--cpu"}
},
new()
{
Name = "Disable Xformers",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = { "--disable-xformers" }
},
new()
{
Name = "Auto-Launch",
Type = LaunchOptionType.Bool,
Options = { "--auto-launch" }
},
LaunchOptionDefinition.Extras
};
new()
{
Name = "Use CPU only",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = { "--cpu" }
},
new()
{
Name = "Disable Xformers",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = { "--disable-xformers" }
},
new()
{
Name = "Auto-Launch",
Type = LaunchOptionType.Bool,
Options = { "--auto-launch" }
},
LaunchOptionDefinition.Extras
};
public override Task<string> GetLatestVersion() => Task.FromResult("master");
public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true)
{
var allBranches = await GetAllBranches().ConfigureAwait(false);
return allBranches.Select(b => new PackageVersion
{
TagName = $"{b.Name}",
ReleaseNotesMarkdown = string.Empty
});
}
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
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));
// Setup venv
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation;
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false);
// Install torch / xformers based on gpu info
switch (torchVersion)
{
case TorchVersion.Cpu:
await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
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);
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);
progress?.Report(
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))
{
progress?.Report(new ProgressReport(-1, "Installing PyTorch for CUDA", isIndeterminate: true));
Logger.Info("Starting torch install (CUDA)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput)
.ConfigureAwait(false);
Logger.Info("Installing xformers...");
await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false);
await InstallCudaTorch(venvRunner, progress).ConfigureAwait(false);
}
else if (HardwareHelper.PreferRocm())
{
progress?.Report(new ProgressReport(-1, "Installing PyTorch for ROCm", isIndeterminate: true));
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, OnConsoleOutput)
.ConfigureAwait(false);
await InstallRocmTorch(venvRunner, progress).ConfigureAwait(false);
}
else if (HardwareHelper.PreferDirectML())
{
await InstallDirectMlTorch(venvRunner, progress).ConfigureAwait(false);
}
else
{
progress?.Report(new ProgressReport(-1, "Installing PyTorch for CPU", isIndeterminate: true));
Logger.Info("Starting torch install (CPU)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput)
.ConfigureAwait(false);
await InstallCpuTorch(venvRunner, progress).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);
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
onConsoleOutput?.Invoke(s);
if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase))
{
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
@ -174,14 +207,22 @@ public class ComfyUI : BaseGitPackage
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner?.RunDetached(
args.TrimEnd(),
HandleConsoleOutput,
HandleExit);
VenvRunner?.RunDetached(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 modelsDir = SettingsManager.ModelsDirectory;
@ -197,19 +238,22 @@ public class ComfyUI : BaseGitPackage
File.WriteAllText(extraPathsYamlPath, string.Empty);
}
var yaml = File.ReadAllText(extraPathsYamlPath);
var comfyModelPaths = deserializer.Deserialize<ComfyModelPathsYaml>(yaml) ??
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
// cuz it can actually be null lol
new ComfyModelPathsYaml();
var comfyModelPaths =
deserializer.Deserialize<ComfyModelPathsYaml>(yaml)
??
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
// cuz it can actually be null lol
new ComfyModelPathsYaml();
comfyModelPaths.StabilityMatrix ??= new ComfyModelPathsYaml.SmData();
comfyModelPaths.StabilityMatrix.Checkpoints = Path.Combine(modelsDir, "StableDiffusion");
comfyModelPaths.StabilityMatrix.Vae = Path.Combine(modelsDir, "VAE");
comfyModelPaths.StabilityMatrix.Loras = $"{Path.Combine(modelsDir, "Lora")}\n" +
$"{Path.Combine(modelsDir, "LyCORIS")}";
comfyModelPaths.StabilityMatrix.UpscaleModels = $"{Path.Combine(modelsDir, "ESRGAN")}\n" +
$"{Path.Combine(modelsDir, "RealESRGAN")}\n" +
$"{Path.Combine(modelsDir, "SwinIR")}";
comfyModelPaths.StabilityMatrix.Loras =
$"{Path.Combine(modelsDir, "Lora")}\n" + $"{Path.Combine(modelsDir, "LyCORIS")}";
comfyModelPaths.StabilityMatrix.UpscaleModels =
$"{Path.Combine(modelsDir, "ESRGAN")}\n"
+ $"{Path.Combine(modelsDir, "RealESRGAN")}\n"
+ $"{Path.Combine(modelsDir, "SwinIR")}";
comfyModelPaths.StabilityMatrix.Embeddings = Path.Combine(modelsDir, "TextualInversion");
comfyModelPaths.StabilityMatrix.Hypernetworks = Path.Combine(modelsDir, "Hypernetwork");
comfyModelPaths.StabilityMatrix.Controlnet = Path.Combine(modelsDir, "ControlNet");
@ -217,7 +261,7 @@ public class ComfyUI : BaseGitPackage
comfyModelPaths.StabilityMatrix.Diffusers = Path.Combine(modelsDir, "Diffusers");
comfyModelPaths.StabilityMatrix.Gligen = Path.Combine(modelsDir, "GLIGEN");
comfyModelPaths.StabilityMatrix.VaeApprox = Path.Combine(modelsDir, "ApproxVAE");
var serializer = new SerializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
@ -227,11 +271,42 @@ public class ComfyUI : BaseGitPackage
return Task.CompletedTask;
}
public override Task UpdateModelFolders(DirectoryPath installDirectory) =>
SetupModelFolders(installDirectory);
public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) => SetupModelFolders(installDirectory, sharedFolderMethod);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) =>
Task.CompletedTask;
public override Task RemoveModelFolderLinks(
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
{

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

@ -1,61 +1,74 @@
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
public class DankDiffusion : BaseGitPackage
{
public DankDiffusion(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) :
base(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
}
public DankDiffusion(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
public override string Name => "dank-diffusion";
public override string DisplayName { get; set; } = "Dank Diffusion";
public override string Author => "mohnjiles";
public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl =>
public override string LicenseUrl =>
"https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE";
public override string Blurb => "A dank interface for diffusion";
public override string LaunchCommand => "test";
public override IReadOnlyList<string> ExtraLaunchCommands => new[]
{
"test-config",
};
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IReadOnlyList<string> ExtraLaunchCommands => new[] { "test-config", };
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();
}
public override Task SetupModelFolders(DirectoryPath installDirectory)
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
public override Task UpdateModelFolders(DirectoryPath installDirectory)
public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
public override IEnumerable<TorchVersion> AvailableTorchVersions { 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();
}

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

@ -10,11 +10,13 @@ namespace StabilityMatrix.Core.Models.Packages;
public class Fooocus : BaseGitPackage
{
public Fooocus(IGithubApiCache githubApi, ISettingsManager settingsManager,
IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper) : base(githubApi,
settingsManager, downloadService, prerequisiteHelper)
{
}
public Fooocus(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
public override string Name => "Fooocus";
public override string DisplayName { get; set; } = "Fooocus";
@ -28,72 +30,124 @@ public class Fooocus : BaseGitPackage
public override string LaunchCommand => "launch.py";
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()
{
LaunchOptionDefinition.Extras
};
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
};
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.Diffusers] = new[] { "models/diffusers" },
[SharedFolderType.Lora] = new[] { "models/loras" },
[SharedFolderType.CLIP] = new[] { "models/clip" },
[SharedFolderType.TextualInversion] = new[] { "models/embeddings" },
[SharedFolderType.VAE] = new[] { "models/vae" },
[SharedFolderType.ApproxVAE] = new[] { "models/vae_approx" },
[SharedFolderType.ControlNet] = new[] { "models/controlnet" },
[SharedFolderType.GLIGEN] = new[] { "models/gligen" },
[SharedFolderType.ESRGAN] = new[] { "models/upscale_models" },
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }
};
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm };
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
{
[SharedFolderType.StableDiffusion] = new[] {"models/checkpoints"},
[SharedFolderType.Diffusers] = new[] {"models/diffusers"},
[SharedFolderType.Lora] = new[] {"models/loras"},
[SharedFolderType.CLIP] = new[] {"models/clip"},
[SharedFolderType.TextualInversion] = new[] {"models/embeddings"},
[SharedFolderType.VAE] = new[] {"models/vae"},
[SharedFolderType.ApproxVAE] = new[] {"models/vae_approx"},
[SharedFolderType.ControlNet] = new[] {"models/controlnet"},
[SharedFolderType.GLIGEN] = new[] {"models/gligen"},
[SharedFolderType.ESRGAN] = new[] {"models/upscale_models"},
[SharedFolderType.Hypernetwork] = new[] {"models/hypernetworks"}
};
public override async Task<string> GetLatestVersion()
{
var release = await GetLatestRelease().ConfigureAwait(false);
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);
var venvRunner = await SetupVenv(InstallLocation, forceRecreate: true).ConfigureAwait(false);
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true));
var torchVersion = "cpu";
var gpus = HardwareHelper.IterGpuInfo().ToList();
if (gpus.Any(g => g.IsNvidia))
{
torchVersion = "cu118";
}
else if (HardwareHelper.PreferRocm())
var torchVersionStr = "cpu";
switch (torchVersion)
{
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
.PipInstall(
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersion}",
OnConsoleOutput).ConfigureAwait(false);
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}",
onConsoleOutput
)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing requirements...",
isIndeterminate: true));
await venvRunner.PipInstall("-r requirements_versions.txt", OnConsoleOutput)
progress?.Report(
new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)
);
await venvRunner
.PipInstall("-r requirements_versions.txt", onConsoleOutput)
.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);
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
onConsoleOutput?.Invoke(s);
if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase))
{
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
@ -114,9 +168,6 @@ public class Fooocus : BaseGitPackage
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner?.RunDetached(
args.TrimEnd(),
HandleConsoleOutput,
HandleExit);
VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
}
}

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

@ -21,183 +21,219 @@ public class InvokeAI : BaseGitPackage
public override string Author => "invoke-ai";
public override string LicenseType => "Apache-2.0";
public override string LicenseUrl =>
"https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE";
public override string LicenseUrl => "https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE";
public override string Blurb => "Professional Creative Tools for Stable Diffusion";
public override string LaunchCommand => "invokeai-web";
public override IReadOnlyList<string> ExtraLaunchCommands => new[]
{
"invokeai-configure",
"invokeai-merge",
"invokeai-metadata",
"invokeai-model-install",
"invokeai-node-cli",
"invokeai-ti",
"invokeai-update",
};
public override Uri PreviewImageUri => new(
"https://raw.githubusercontent.com/invoke-ai/InvokeAI/main/docs/assets/canvas_preview.png");
public override IReadOnlyList<string> ExtraLaunchCommands =>
new[]
{
"invokeai-configure",
"invokeai-merge",
"invokeai-metadata",
"invokeai-model-install",
"invokeai-node-cli",
"invokeai-ti",
"invokeai-update",
};
public override Uri PreviewImageUri =>
new(
"https://raw.githubusercontent.com/invoke-ai/InvokeAI/main/docs/assets/canvas_preview.png"
);
public override bool ShouldIgnoreReleases => true;
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public InvokeAI(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) :
base(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
}
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
{
[SharedFolderType.StableDiffusion] = new[] { RelativeRootPath + "/autoimport/main" },
[SharedFolderType.Lora] = new[] { RelativeRootPath + "/autoimport/lora" },
[SharedFolderType.TextualInversion] = new[] { RelativeRootPath + "/autoimport/embedding" },
[SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" },
};
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
// https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md
public override List<LaunchOptionDefinition> LaunchOptions => new List<LaunchOptionDefinition>
{
new()
{
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new List<string> {"--host"}
},
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "9090",
Options = new List<string> {"--port"}
},
new()
{
Name = "Allow Origins",
Description = "List of host names or IP addresses that are allowed to connect to the " +
"InvokeAI API in the format ['host1','host2',...]",
Type = LaunchOptionType.String,
DefaultValue = "[]",
Options = new List<string> {"--allow-origins"}
},
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
Name = "Always use CPU",
Type = LaunchOptionType.Bool,
Options = new List<string> {"--always_use_cpu"}
},
new()
{
Name = "Precision",
Type = LaunchOptionType.Bool,
Options = new List<string>
[SharedFolderType.StableDiffusion] = new[] { RelativeRootPath + "/autoimport/main" },
[SharedFolderType.Lora] = new[] { RelativeRootPath + "/autoimport/lora" },
[SharedFolderType.TextualInversion] = new[]
{
"--precision auto",
"--precision float16",
"--precision float32",
}
},
new()
RelativeRootPath + "/autoimport/embedding"
},
[SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" },
};
// https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{
Name = "Aggressively free up GPU memory after each operation",
Type = LaunchOptionType.Bool,
Options = new List<string> {"--free_gpu_mem"}
},
LaunchOptionDefinition.Extras
};
new()
{
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new List<string> { "--host" }
},
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "9090",
Options = new List<string> { "--port" }
},
new()
{
Name = "Allow Origins",
Description =
"List of host names or IP addresses that are allowed to connect to the "
+ "InvokeAI API in the format ['host1','host2',...]",
Type = LaunchOptionType.String,
DefaultValue = "[]",
Options = new List<string> { "--allow-origins" }
},
new()
{
Name = "Always use CPU",
Type = LaunchOptionType.Bool,
Options = new List<string> { "--always_use_cpu" }
},
new()
{
Name = "Precision",
Type = LaunchOptionType.Bool,
Options = new List<string>
{
"--precision auto",
"--precision float16",
"--precision float32",
}
},
new()
{
Name = "Aggressively free up GPU memory after each operation",
Type = LaunchOptionType.Bool,
Options = new List<string> { "--free_gpu_mem" }
},
LaunchOptionDefinition.Extras
};
public override Task<string> GetLatestVersion() => Task.FromResult("main");
public override Task<string> DownloadPackage(string version, bool isCommitHash, string? branch,
IProgress<ProgressReport>? progress = null)
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
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
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation;
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false);
venvRunner.EnvironmentVariables = GetEnvVars(InstallLocation);
var gpus = HardwareHelper.IterGpuInfo().ToList();
venvRunner.EnvironmentVariables = GetEnvVars(installLocation);
progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true));
var pipCommandArgs =
"InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu";
// If has Nvidia Gpu, install CUDA version
if (gpus.Any(g => g.IsNvidia))
switch (torchVersion)
{
Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs =
"InvokeAI[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117";
}
// For AMD, Install ROCm version
else if (HardwareHelper.PreferRocm())
{
Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs =
"InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
}
// For Apple silicon, use MPS
else if (Compat.IsMacOS && Compat.IsArm)
{
Logger.Info("Starting InvokeAI install (MPS)...");
pipCommandArgs = "InvokeAI --use-pep517";
}
// CPU Version
else
{
Logger.Info("Starting InvokeAI install (CPU)...");
case TorchVersion.Cuda:
Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs =
"InvokeAI[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117";
break;
case TorchVersion.Rocm:
Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs =
"InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
break;
case TorchVersion.Mps:
Logger.Info("Starting InvokeAI install (MPS)...");
pipCommandArgs = "InvokeAI --use-pep517";
break;
}
await venvRunner.PipInstall(pipCommandArgs, OnConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall(pipCommandArgs, onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall("rich packaging python-dotenv", OnConsoleOutput)
.PipInstall("rich packaging python-dotenv", onConsoleOutput)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Configuring InvokeAI", isIndeterminate: true));
await RunInvokeCommand(InstallLocation, "invokeai-configure", "--yes --skip-sd-weights",
false).ConfigureAwait(false);
await RunInvokeCommand(
installLocation,
"invokeai-configure",
"--yes --skip-sd-weights",
false,
onConsoleOutput
)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false));
}
public override async Task<string> Update(InstalledPackage installedPackage, IProgress<ProgressReport>? progress = null,
bool includePrerelease = false)
public override async Task<InstalledPackageVersion> Update(
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));
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.EnvironmentVariables = GetEnvVars(installedPackage.FullPath);
var latestVersion = await GetUpdateVersion(installedPackage).ConfigureAwait(false);
var isReleaseMode = string.IsNullOrWhiteSpace(installedPackage.InstalledBranch);
var isReleaseMode = installedPackage.Version.IsReleaseMode;
var downloadUrl = isReleaseMode
? $"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();
@ -206,60 +242,75 @@ public class InvokeAI : BaseGitPackage
var pipCommandArgs =
$"\"InvokeAI @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu --upgrade";
// If has Nvidia Gpu, install CUDA version
if (gpus.Any(g => g.IsNvidia))
{
Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs =
$"\"InvokeAI[xformers] @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117 --upgrade";
}
// For AMD, Install ROCm version
else if (gpus.Any(g => g.IsAmd))
{
Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs =
$"\"InvokeAI @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2 --upgrade";
}
// For Apple silicon, use MPS
else if (Compat.IsMacOS && Compat.IsArm)
{
Logger.Info("Starting InvokeAI install (MPS)...");
pipCommandArgs = $"\"InvokeAI @ {downloadUrl}\" --use-pep517 --upgrade";
}
// CPU Version
else
switch (torchVersion)
{
Logger.Info("Starting InvokeAI install (CPU)...");
// If has Nvidia Gpu, install CUDA version
case TorchVersion.Cuda:
Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs =
$"\"InvokeAI[xformers] @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117 --upgrade";
break;
// For AMD, Install ROCm version
case TorchVersion.Rocm:
Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs =
$"\"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
Logger.Info("Starting InvokeAI install (MPS)...");
pipCommandArgs = $"\"InvokeAI @ {downloadUrl}\" --use-pep517 --upgrade";
break;
}
await venvRunner.PipInstall(pipCommandArgs, OnConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall(pipCommandArgs, onConsoleOutput).ConfigureAwait(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
RunPackage(string installedPackagePath, string command, string arguments) =>
RunInvokeCommand(installedPackagePath, command, arguments, true);
private async Task<string> GetUpdateVersion(InstalledPackage installedPackage, bool includePrerelease = false)
public override Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
) => RunInvokeCommand(installedPackagePath, command, arguments, true, onConsoleOutput);
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 latestRelease = releases.First(x => includePrerelease || !x.Prerelease);
return latestRelease.TagName;
}
var allCommits = await GetAllCommits(
installedPackage.InstalledBranch).ConfigureAwait(false);
var allCommits = await GetAllCommits(installedPackage.Version.InstalledBranch)
.ConfigureAwait(false);
var latestCommit = allCommits.First();
return latestCommit.Sha;
}
private async Task RunInvokeCommand(string installedPackagePath, string command,
string arguments, bool runDetached)
private async Task RunInvokeCommand(
string installedPackagePath,
string command,
string arguments,
bool runDetached,
Action<ProcessOutput>? onConsoleOutput
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
@ -278,12 +329,12 @@ public class InvokeAI : BaseGitPackage
// Split at ':' to get package and function
var split = entryPoint?.Split(':');
if (split is not {Length: > 1})
if (split is not { Length: > 1 })
{
throw new Exception($"Could not find entry point for InvokeAI: {entryPoint.ToRepr()}");
}
// Compile a startup command according to
// Compile a startup command according to
// https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts
// For invokeai, also patch the shutil.get_terminal_size function to return a fixed value
// above the minimum in invokeai.frontend.install.widgets
@ -298,30 +349,32 @@ public class InvokeAI : BaseGitPackage
{
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;
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
var match = regex.Match(s.Text);
if (!match.Success)
return;
WebUrl = match.Value;
OnStartupComplete(WebUrl);
}
VenvRunner.RunDetached($"-c \"{code}\" {arguments}".TrimEnd(), HandleConsoleOutput, OnExit);
VenvRunner.RunDetached(
$"-c \"{code}\" {arguments}".TrimEnd(),
HandleConsoleOutput,
OnExit
);
}
else
{
var result = await VenvRunner.Run($"-c \"{code}\" {arguments}".TrimEnd())
var result = await VenvRunner
.Run($"-c \"{code}\" {arguments}".TrimEnd())
.ConfigureAwait(false);
OnConsoleOutput(new ProcessOutput
{
Text = result.StandardOutput
});
onConsoleOutput?.Invoke(new ProcessOutput { 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; }
}

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

@ -2,6 +2,7 @@
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Core.Models.Packages;
@ -14,53 +15,77 @@ public class UnknownPackage : BasePackage
public override string GithubUrl => "";
public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl =>
public override string LicenseUrl =>
"https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE";
public override string Blurb => "A dank interface for diffusion";
public override string LaunchCommand => "test";
public override Uri PreviewImageUri => new("");
public override IReadOnlyList<string> ExtraLaunchCommands => new[]
{
"test-config",
};
/// <inheritdoc />
public override Task<string> DownloadPackage(string version, bool isCommitHash, string? branch, IProgress<ProgressReport>? progress = null)
public override IReadOnlyList<string> ExtraLaunchCommands => new[] { "test-config", };
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress1
)
{
throw new NotImplementedException();
}
/// <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();
}
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();
}
/// <inheritdoc />
public override Task SetupModelFolders(DirectoryPath installDirectory)
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task UpdateModelFolders(DirectoryPath installDirectory)
public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cuda, TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl };
/// <inheritdoc />
public override void Shutdown()
{
@ -80,26 +105,40 @@ public class UnknownPackage : BasePackage
}
/// <inheritdoc />
public override Task<string> Update(InstalledPackage installedPackage, IProgress<ProgressReport>? progress = null,
bool includePrerelease = false)
public override Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
)
{
throw new NotImplementedException();
}
/// <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 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 />
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 />
public override Task<IEnumerable<Branch>> GetAllBranches() => Task.FromResult(Enumerable.Empty<Branch>());
public override Task<IEnumerable<Branch>> GetAllBranches() =>
Task.FromResult(Enumerable.Empty<Branch>());
/// <inheritdoc />
public override Task<IEnumerable<Release>> GetAllReleases() => Task.FromResult(Enumerable.Empty<Release>());
public override Task<IEnumerable<Release>> GetAllReleases() =>
Task.FromResult(Enumerable.Empty<Release>());
}

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

@ -1,5 +1,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using NLog;
using StabilityMatrix.Core.Helper;
@ -15,12 +17,12 @@ namespace StabilityMatrix.Core.Models.Packages;
public class VladAutomatic : BaseGitPackage
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public override string Name => "automatic";
public override string DisplayName { get; set; } = "SD.Next Web UI";
public override string Author => "vladmandic";
public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl =>
public override string LicenseUrl =>
"https://github.com/vladmandic/automatic/blob/master/LICENSE.txt";
public override string Blurb => "Stable Diffusion implementation with advanced features";
public override string LaunchCommand => "launch.py";
@ -29,189 +31,237 @@ public class VladAutomatic : BaseGitPackage
new("https://github.com/vladmandic/automatic/raw/master/html/black-orange.jpg");
public override bool ShouldIgnoreReleases => true;
public VladAutomatic(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) :
base(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
}
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
// https://github.com/vladmandic/automatic/blob/master/modules/shared.py#L324
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
{
[SharedFolderType.StableDiffusion] = new[] {"models/Stable-diffusion"},
[SharedFolderType.Diffusers] = new[] {"models/Diffusers"},
[SharedFolderType.VAE] = new[] {"models/VAE"},
[SharedFolderType.TextualInversion] = new[] {"models/embeddings"},
[SharedFolderType.Hypernetwork] = new[] {"models/hypernetworks"},
[SharedFolderType.Codeformer] = new[] {"models/Codeformer"},
[SharedFolderType.GFPGAN] = new[] {"models/GFPGAN"},
[SharedFolderType.BSRGAN] = new[] {"models/BSRGAN"},
[SharedFolderType.ESRGAN] = new[] {"models/ESRGAN"},
[SharedFolderType.RealESRGAN] = new[] {"models/RealESRGAN"},
[SharedFolderType.ScuNET] = new[] {"models/ScuNET"},
[SharedFolderType.SwinIR] = new[] {"models/SwinIR"},
[SharedFolderType.LDSR] = new[] {"models/LDSR"},
[SharedFolderType.CLIP] = new[] {"models/CLIP"},
[SharedFolderType.Lora] = new[] {"models/Lora"},
[SharedFolderType.LyCORIS] = new[] {"models/LyCORIS"},
[SharedFolderType.ControlNet] = new[] {"models/ControlNet"}
};
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl, TorchVersion.Cuda };
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions => new()
{
new()
{
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new() {"--server-name"}
},
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
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "7860",
Options = new() {"--port"}
},
[SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" },
[SharedFolderType.Diffusers] = new[] { "models/Diffusers" },
[SharedFolderType.VAE] = new[] { "models/VAE" },
[SharedFolderType.TextualInversion] = new[] { "models/embeddings" },
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" },
[SharedFolderType.Codeformer] = new[] { "models/Codeformer" },
[SharedFolderType.GFPGAN] = new[] { "models/GFPGAN" },
[SharedFolderType.BSRGAN] = new[] { "models/BSRGAN" },
[SharedFolderType.ESRGAN] = new[] { "models/ESRGAN" },
[SharedFolderType.RealESRGAN] = new[] { "models/RealESRGAN" },
[SharedFolderType.ScuNET] = new[] { "models/ScuNET" },
[SharedFolderType.SwinIR] = new[] { "models/SwinIR" },
[SharedFolderType.LDSR] = new[] { "models/LDSR" },
[SharedFolderType.CLIP] = new[] { "models/CLIP" },
[SharedFolderType.Lora] = new[] { "models/Lora" },
[SharedFolderType.LyCORIS] = new[] { "models/LyCORIS" },
[SharedFolderType.ControlNet] = new[] { "models/ControlNet" }
};
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
new()
{
Level.Low => "--lowvram",
Level.Medium => "--medvram",
_ => null
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new() { "--server-name" }
},
Options = new() { "--lowvram", "--medvram" }
},
new()
{
Name = "Force use of Intel OneAPI XPU backend",
Type = LaunchOptionType.Bool,
Options = new() { "--use-ipex" }
},
new()
{
Name = "Use DirectML if no compatible GPU is detected",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.PreferDirectML(),
Options = new() { "--use-directml" }
},
new()
{
Name = "Force use of Nvidia CUDA backend",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.HasNvidiaGpu(),
Options = new() { "--use-cuda" }
},
new()
{
Name = "Force use of AMD ROCm backend",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.PreferRocm(),
Options = new() { "--use-rocm" }
},
new()
{
Name = "CUDA Device ID",
Type = LaunchOptionType.String,
Options = new() { "--device-id" }
},
new()
{
Name = "API",
Type = LaunchOptionType.Bool,
Options = new() { "--api" }
},
new()
{
Name = "Debug Logging",
Type = LaunchOptionType.Bool,
Options = new() { "--debug" }
},
LaunchOptionDefinition.Extras
};
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "7860",
Options = new() { "--port" }
},
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{
Level.Low => "--lowvram",
Level.Medium => "--medvram",
_ => null
},
Options = new() { "--lowvram", "--medvram" }
},
new()
{
Name = "Auto-Launch Web UI",
Type = LaunchOptionType.Bool,
Options = new() { "--autolaunch" }
},
new()
{
Name = "Force use of Intel OneAPI XPU backend",
Type = LaunchOptionType.Bool,
Options = new() { "--use-ipex" }
},
new()
{
Name = "Use DirectML if no compatible GPU is detected",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.PreferDirectML(),
Options = new() { "--use-directml" }
},
new()
{
Name = "Force use of Nvidia CUDA backend",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.HasNvidiaGpu(),
Options = new() { "--use-cuda" }
},
new()
{
Name = "Force use of AMD ROCm backend",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.PreferRocm(),
Options = new() { "--use-rocm" }
},
new()
{
Name = "CUDA Device ID",
Type = LaunchOptionType.String,
Options = new() { "--device-id" }
},
new()
{
Name = "API",
Type = LaunchOptionType.Bool,
Options = new() { "--api" }
},
new()
{
Name = "Debug Logging",
Type = LaunchOptionType.Bool,
Options = new() { "--debug" }
},
LaunchOptionDefinition.Extras
};
public override string ExtraLaunchArguments => "";
public override Task<string> GetLatestVersion() => Task.FromResult("master");
public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true)
{
var allBranches = await GetAllBranches();
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
)
{
progress?.Report(new ProgressReport(-1f, "Installing package...", isIndeterminate: true));
// Setup venv
var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation;
var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = installLocation;
venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables;
await venvRunner.Setup(true).ConfigureAwait(false);
// Run initial install
if (HardwareHelper.HasNvidiaGpu())
{
// CUDA
await venvRunner.CustomInstall("launch.py --use-cuda --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
}
else if (HardwareHelper.PreferRocm())
{
// ROCm
await venvRunner.CustomInstall("launch.py --use-rocm --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
}
else if (HardwareHelper.PreferDirectML())
{
// DirectML
await venvRunner.CustomInstall("launch.py --use-directml --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
}
else
switch (torchVersion)
{
await venvRunner.CustomInstall("launch.py --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
// Run initial install
case TorchVersion.Cuda:
await venvRunner
.CustomInstall("launch.py --use-cuda --debug --test", onConsoleOutput)
.ConfigureAwait(false);
break;
case TorchVersion.Rocm:
await venvRunner
.CustomInstall("launch.py --use-rocm --debug --test", onConsoleOutput)
.ConfigureAwait(false);
break;
case TorchVersion.DirectMl:
await venvRunner
.CustomInstall("launch.py --use-directml --debug --test", onConsoleOutput)
.ConfigureAwait(false);
break;
default:
// CPU
await venvRunner
.CustomInstall("launch.py --debug --test", onConsoleOutput)
.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,
string? branch, IProgress<ProgressReport>? progress = null)
public override async Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions downloadOptions,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, message: "Downloading package...",
isIndeterminate: true, type: ProgressType.Download));
progress?.Report(
new ProgressReport(
-1f,
message: "Downloading package...",
isIndeterminate: true,
type: ProgressType.Download
)
);
var installDir = new DirectoryPath(InstallLocation);
var installDir = new DirectoryPath(installLocation);
installDir.Create();
await PrerequisiteHelper
.RunGit(installDir.Parent ?? "", "clone", "https://github.com/vladmandic/automatic",
installDir.Name).ConfigureAwait(false);
await PrerequisiteHelper.RunGit(
InstallLocation, "checkout", version).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(downloadOptions.CommitHash))
{
await PrerequisiteHelper
.RunGit(
installDir.Parent ?? "",
"clone",
"https://github.com/vladmandic/automatic",
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);
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
onConsoleOutput?.Invoke(s);
if (s.Text.Contains("Running on local URL", StringComparison.OrdinalIgnoreCase))
{
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
@ -235,36 +285,51 @@ public class VladAutomatic : BaseGitPackage
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
}
public override async Task<string> Update(InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null, bool includePrerelease = false)
public override async Task<InstalledPackageVersion> Update(
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...",
isIndeterminate: true, type: ProgressType.Update));
await PrerequisiteHelper.RunGit(installedPackage.FullPath, "checkout",
installedPackage.InstalledBranch).ConfigureAwait(false);
progress?.Report(
new ProgressReport(
-1f,
message: "Downloading package update...",
isIndeterminate: true,
type: ProgressType.Update
)
);
await PrerequisiteHelper
.RunGit(installedPackage.FullPath, "checkout", installedPackage.Version.InstalledBranch)
.ConfigureAwait(false);
var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath!, "venv"));
venvRunner.WorkingDirectory = InstallLocation;
venvRunner.WorkingDirectory = installedPackage.FullPath!;
venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables;
await venvRunner.CustomInstall("launch.py --upgrade --test", OnConsoleOutput)
.ConfigureAwait(false);
await venvRunner
.CustomInstall("launch.py --upgrade --test", onConsoleOutput)
.ConfigureAwait(false);
try
{
var output =
await PrerequisiteHelper
.GetGitOutput(installedPackage.FullPath, "rev-parse", "HEAD")
.ConfigureAwait(false);
var output = await PrerequisiteHelper
.GetGitOutput(installedPackage.FullPath, "rev-parse", "HEAD")
.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)
{
@ -272,10 +337,113 @@ public class VladAutomatic : BaseGitPackage
}
finally
{
progress?.Report(new ProgressReport(1f, message: "Update Complete", isIndeterminate: false,
type: ProgressType.Update));
progress?.Report(
new ProgressReport(
1f,
message: "Update Complete",
isIndeterminate: false,
type: ProgressType.Update
)
);
}
return installedPackage.InstalledBranch;
return new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.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
};
}

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

@ -14,146 +14,170 @@ public class VoltaML : BaseGitPackage
public override string DisplayName { get; set; } = "VoltaML";
public override string Author => "VoltaML";
public override string LicenseType => "GPL-3.0";
public override string LicenseUrl =>
public override string LicenseUrl =>
"https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/License";
public override string Blurb => "Fast Stable Diffusion with support for AITemplate";
public override string LaunchCommand => "main.py";
public override Uri PreviewImageUri => new(
"https://github.com/LykosAI/StabilityMatrix/assets/13956642/d9a908ed-5665-41a5-a380-98458f4679a8");
public override Uri PreviewImageUri =>
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,
// so we'll just limit to commit mode to be more consistent
public override bool ShouldIgnoreReleases => true;
public VoltaML(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) :
base(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
}
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
// https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L86
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
{
[SharedFolderType.StableDiffusion] = new[] {"data/models"},
[SharedFolderType.Lora] = new[] {"data/lora"},
[SharedFolderType.TextualInversion] = new[] {"data/textual-inversion"},
};
// https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L45
public override List<LaunchOptionDefinition> LaunchOptions => new List<LaunchOptionDefinition>
{
new()
{
Name = "Log Level",
Type = LaunchOptionType.Bool,
DefaultValue = "--log-level INFO",
Options =
{
"--log-level DEBUG",
"--log-level INFO",
"--log-level WARNING",
"--log-level ERROR",
"--log-level CRITICAL"
}
},
new()
{
Name = "Use ngrok to expose the API",
Type = LaunchOptionType.Bool,
Options = {"--ngrok"}
},
new()
{
Name = "Expose the API to the network",
Type = LaunchOptionType.Bool,
Options = {"--host"}
},
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
Name = "Skip virtualenv check",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = {"--in-container"}
},
new()
[SharedFolderType.StableDiffusion] = new[] { "data/models" },
[SharedFolderType.Lora] = new[] { "data/lora" },
[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
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{
Name = "Force VoltaML to use a specific type of PyTorch distribution",
Type = LaunchOptionType.Bool,
Options =
new()
{
"--pytorch-type cpu",
"--pytorch-type cuda",
"--pytorch-type rocm",
"--pytorch-type directml",
"--pytorch-type intel",
"--pytorch-type vulkan"
}
},
new()
{
Name = "Run in tandem with the Discord bot",
Type = LaunchOptionType.Bool,
Options = {"--bot"}
},
new()
{
Name = "Enable Cloudflare R2 bucket upload support",
Type = LaunchOptionType.Bool,
Options = {"--enable-r2"}
},
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "5003",
Options = {"--port"}
},
new()
{
Name = "Only install requirements and exit",
Type = LaunchOptionType.Bool,
Options = {"--install-only"}
},
LaunchOptionDefinition.Extras
};
Name = "Log Level",
Type = LaunchOptionType.Bool,
DefaultValue = "--log-level INFO",
Options =
{
"--log-level DEBUG",
"--log-level INFO",
"--log-level WARNING",
"--log-level ERROR",
"--log-level CRITICAL"
}
},
new()
{
Name = "Use ngrok to expose the API",
Type = LaunchOptionType.Bool,
Options = { "--ngrok" }
},
new()
{
Name = "Expose the API to the network",
Type = LaunchOptionType.Bool,
Options = { "--host" }
},
new()
{
Name = "Skip virtualenv check",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = { "--in-container" }
},
new()
{
Name = "Force VoltaML to use a specific type of PyTorch distribution",
Type = LaunchOptionType.Bool,
Options =
{
"--pytorch-type cpu",
"--pytorch-type cuda",
"--pytorch-type rocm",
"--pytorch-type directml",
"--pytorch-type intel",
"--pytorch-type vulkan"
}
},
new()
{
Name = "Run in tandem with the Discord bot",
Type = LaunchOptionType.Bool,
Options = { "--bot" }
},
new()
{
Name = "Enable Cloudflare R2 bucket upload support",
Type = LaunchOptionType.Bool,
Options = { "--enable-r2" }
},
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "5003",
Options = { "--port" }
},
new()
{
Name = "Only install requirements and exit",
Type = LaunchOptionType.Bool,
Options = { "--install-only" }
},
LaunchOptionDefinition.Extras
};
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
progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true));
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation;
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false);
// Install requirements
progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true));
progress?.Report(
new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)
);
await venvRunner
.PipInstall("rich packaging python-dotenv", OnConsoleOutput)
.PipInstall("rich packaging python-dotenv", onConsoleOutput)
.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);
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
var foundIndicator = false;
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
onConsoleOutput?.Invoke(s);
if (s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase))
{
@ -164,17 +188,17 @@ public class VoltaML : BaseGitPackage
if (!foundIndicator)
return;
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
var match = regex.Match(s.Text);
if (!match.Success)
return;
WebUrl = match.Value;
OnStartupComplete(WebUrl);
foundIndicator = false;
}
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, OnExit);
}
}

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

@ -1,3 +1,8 @@
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
}

2
StabilityMatrix.Core/Services/IModelIndexService.cs

@ -14,7 +14,7 @@ public interface IModelIndexService
/// Get all models of the specified type from the existing index.
/// </summary>
Task<IReadOnlyList<LocalModelFile>> GetModelsOfType(SharedFolderType type);
/// <summary>
/// Starts a background task to refresh the local model file index.
/// </summary>

14
StabilityMatrix.Core/Services/ISettingsManager.cs

@ -15,6 +15,7 @@ public interface ISettingsManager
string ModelsDirectory { get; }
string DownloadsDirectory { get; }
Settings Settings { get; }
List<string> PackageInstallsInProgress { get; set; }
event EventHandler<string>? LibraryDirChanged;
event EventHandler<PropertyChangedEventArgs>? SettingsPropertyChanged;
@ -23,7 +24,7 @@ public interface ISettingsManager
/// Will fire instantly if it is already set.
/// </summary>
void RegisterOnLibraryDirSet(Action<string> handler);
/// <inheritdoc />
SettingsTransaction BeginTransaction();
@ -35,14 +36,17 @@ public interface ISettingsManager
/// <inheritdoc />
void RelayPropertyFor<T, TValue>(
T source,
T source,
Expression<Func<T, TValue>> sourceProperty,
Expression<Func<Settings, TValue>> settingsProperty) where T : INotifyPropertyChanged;
Expression<Func<Settings, TValue>> settingsProperty
)
where T : INotifyPropertyChanged;
/// <inheritdoc />
void RegisterPropertyChangedHandler<T>(
Expression<Func<Settings, T>> settingsProperty,
Action<T> onPropertyChanged);
Action<T> onPropertyChanged
);
/// <summary>
/// Attempts to locate and set the library path
@ -76,7 +80,7 @@ public interface ISettingsManager
/// </summary>
void InsertPathExtensions();
void UpdatePackageVersionNumber(Guid id, string? newVersion);
void UpdatePackageVersionNumber(Guid id, InstalledPackageVersion? newVersion);
void SetLastUpdateCheck(InstalledPackage package);
List<LaunchOption> GetLaunchArgs(Guid packageId);
void SaveLaunchArgs(Guid packageId, List<LaunchOption> launchArgs);

71
StabilityMatrix.Core/Services/ModelIndexService.cs

@ -38,9 +38,10 @@ public class ModelIndexService : IModelIndexService
return await liteDbContext.LocalModelFiles
.Query()
.Where(m => m.SharedFolderType == type)
.ToArrayAsync().ConfigureAwait(false);
.ToArrayAsync()
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task RefreshIndex()
{
@ -49,19 +50,18 @@ public class ModelIndexService : IModelIndexService
// Start
var stopwatch = Stopwatch.StartNew();
logger.LogInformation("Refreshing model index...");
using var db
= await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
using var db = await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
var localModelFiles = db.GetCollection<LocalModelFile>("LocalModelFiles")!;
await localModelFiles.DeleteAllAsync().ConfigureAwait(false);
// Record start of actual indexing
var indexStart = stopwatch.Elapsed;
var added = 0;
foreach (
var file in modelsDir.Info
.EnumerateFiles("*.*", SearchOption.AllDirectories)
@ -73,61 +73,74 @@ public class ModelIndexService : IModelIndexService
{
continue;
}
var relativePath = Path.GetRelativePath(modelsDir, file);
// Get shared folder name
var sharedFolderName = relativePath.Split(Path.DirectorySeparatorChar,
StringSplitOptions.RemoveEmptyEntries)[0];
var sharedFolderName = relativePath.Split(
Path.DirectorySeparatorChar,
StringSplitOptions.RemoveEmptyEntries
)[0];
// Convert to enum
var sharedFolderType = Enum.Parse<SharedFolderType>(sharedFolderName, true);
var localModel = new LocalModelFile
{
RelativePath = relativePath,
SharedFolderType = sharedFolderType,
};
// Try to find a connected model info
var jsonPath = file.Directory!.JoinFile(
new FilePath(file.NameWithoutExtension, ".cm-info.json"));
new FilePath(file.NameWithoutExtension, ".cm-info.json")
);
if (jsonPath.Exists)
{
var connectedModelInfo = ConnectedModelInfo.FromJson(
await jsonPath.ReadAllTextAsync().ConfigureAwait(false));
await jsonPath.ReadAllTextAsync().ConfigureAwait(false)
);
localModel.ConnectedModelInfo = connectedModelInfo;
}
// Try to find a preview image
var previewImagePath = LocalModelFile.SupportedImageExtensions
.Select(ext => file.Directory!.JoinFile($"{file.NameWithoutExtension}.preview{ext}")
).FirstOrDefault(path => path.Exists);
.Select(
ext => file.Directory!.JoinFile($"{file.NameWithoutExtension}.preview{ext}")
)
.FirstOrDefault(path => path.Exists);
if (previewImagePath != null)
{
localModel.PreviewImageRelativePath = Path.GetRelativePath(modelsDir, previewImagePath);
localModel.PreviewImageRelativePath = Path.GetRelativePath(
modelsDir,
previewImagePath
);
}
// Insert into database
await localModelFiles.InsertAsync(localModel).ConfigureAwait(false);
added++;
}
// Record end of actual indexing
var indexEnd = stopwatch.Elapsed;
await db.CommitAsync().ConfigureAwait(false);
// End
stopwatch.Stop();
var indexDuration = indexEnd - indexStart;
var dbDuration = stopwatch.Elapsed - indexDuration;
logger.LogInformation("Model index refreshed with {Entries} entries, took {IndexDuration:F1}ms ({DbDuration:F1}ms db)",
added, indexDuration.TotalMilliseconds, dbDuration.TotalMilliseconds);
logger.LogInformation(
"Model index refreshed with {Entries} entries, took {IndexDuration:F1}ms ({DbDuration:F1}ms db)",
added,
indexDuration.TotalMilliseconds,
dbDuration.TotalMilliseconds
);
}
/// <inheritdoc />

268
StabilityMatrix.Core/Services/SettingsManager.cs

@ -20,10 +20,16 @@ public class SettingsManager : ISettingsManager
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static readonly ReaderWriterLockSlim FileLock = new();
private static readonly string GlobalSettingsPath = Path.Combine(Compat.AppDataHome, "global.json");
private readonly string? originalEnvPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process);
private static readonly string GlobalSettingsPath = Path.Combine(
Compat.AppDataHome,
"global.json"
);
private readonly string? originalEnvPath = Environment.GetEnvironmentVariable(
"PATH",
EnvironmentVariableTarget.Process
);
// Library properties
public bool IsPortableMode { get; private set; }
private string? libraryDir;
@ -50,10 +56,11 @@ public class SettingsManager : ISettingsManager
private string SettingsPath => Path.Combine(LibraryDir, "settings.json");
public string ModelsDirectory => Path.Combine(LibraryDir, "Models");
public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads");
public List<string> PackageInstallsInProgress { get; set; } = new();
public Settings Settings { get; private set; } = new();
public event EventHandler<string>? LibraryDirChanged;
public event EventHandler<string>? LibraryDirChanged;
public event EventHandler<PropertyChangedEventArgs>? SettingsPropertyChanged;
/// <inheritdoc />
@ -66,7 +73,7 @@ public class SettingsManager : ISettingsManager
}
LibraryDirChanged += Handler;
return;
void Handler(object? sender, string dir)
@ -75,17 +82,19 @@ public class SettingsManager : ISettingsManager
handler(dir);
}
}
/// <inheritdoc />
public SettingsTransaction BeginTransaction()
{
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);
}
/// <inheritdoc />
public void Transaction(Action<Settings> func, bool ignoreMissingLibraryDir = false)
{
@ -102,97 +111,111 @@ public class SettingsManager : ISettingsManager
func(transaction.Settings);
transaction.Dispose();
}
/// <inheritdoc />
public void Transaction<TValue>(Expression<Func<Settings, TValue>> expression, TValue value)
{
if (expression.Body is not MemberExpression memberExpression)
{
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;
if (propertyInfo == null)
{
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;
// Set value
using var transaction = BeginTransaction();
propertyInfo.SetValue(transaction.Settings, value);
// Invoke property changed event
SettingsPropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
/// <inheritdoc />
public void RelayPropertyFor<T, TValue>(
T source,
T source,
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 (propertyName, assigner) = Expressions.GetAssigner(sourceProperty);
var sourceSetter = assigner.Compile();
var settingsGetter = settingsProperty.Compile();
var (targetPropertyName, settingsAssigner) = Expressions.GetAssigner(settingsProperty);
var settingsSetter = settingsAssigner.Compile();
var sourceTypeName = source.GetType().Name;
// Update source when settings change
SettingsPropertyChanged += (_, args) =>
{
if (args.PropertyName != propertyName) return;
if (args.PropertyName != propertyName)
return;
Logger.Trace(
"[RelayPropertyFor] " +
"Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}",
targetPropertyName, sourceTypeName, propertyName);
"[RelayPropertyFor] "
+ "Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}",
targetPropertyName,
sourceTypeName,
propertyName
);
sourceSetter(source, settingsGetter(Settings));
};
// Set and Save settings when source changes
source.PropertyChanged += (_, args) =>
{
if (args.PropertyName != propertyName) return;
if (args.PropertyName != propertyName)
return;
Logger.Trace(
"[RelayPropertyFor] " +
"{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}",
sourceTypeName, propertyName, targetPropertyName);
"[RelayPropertyFor] "
+ "{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}",
sourceTypeName,
propertyName,
targetPropertyName
);
settingsSetter(Settings, sourceGetter(source));
SaveSettingsAsync().SafeFireAndForget();
// Invoke property changed event
SettingsPropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
};
}
/// <inheritdoc />
public void RegisterPropertyChangedHandler<T>(
Expression<Func<Settings, T>> settingsProperty,
Action<T> onPropertyChanged)
Action<T> onPropertyChanged
)
{
var settingsGetter = settingsProperty.Compile();
var (propertyName, _) = Expressions.GetAssigner(settingsProperty);
// Invoke handler when settings change
SettingsPropertyChanged += (_, args) =>
{
if (args.PropertyName != propertyName) return;
if (args.PropertyName != propertyName)
return;
onPropertyChanged(settingsGetter(Settings));
};
}
/// <summary>
/// Attempts to locate and set the library path
/// Return true if found, false otherwise
@ -209,18 +232,21 @@ public class SettingsManager : ISettingsManager
LoadSettings();
return true;
}
// 2. Check %APPDATA%/StabilityMatrix/library.json
FilePath libraryJsonFile = Compat.AppDataHome + "library.json";
if (!libraryJsonFile.Exists) return false;
if (!libraryJsonFile.Exists)
return false;
try
{
var libraryJson = libraryJsonFile.ReadAllText();
var librarySettings = JsonSerializer.Deserialize<LibrarySettings>(libraryJson);
if (!string.IsNullOrWhiteSpace(librarySettings?.LibraryPath)
&& Directory.Exists(librarySettings?.LibraryPath))
if (
!string.IsNullOrWhiteSpace(librarySettings?.LibraryPath)
&& Directory.Exists(librarySettings?.LibraryPath)
)
{
LibraryDir = librarySettings.LibraryPath;
SetStaticLibraryPaths();
@ -252,13 +278,16 @@ public class SettingsManager : ISettingsManager
var libraryJsonFile = Compat.AppDataHome.JoinFile("library.json");
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);
// actually create the LibraryPath directory
Directory.CreateDirectory(path);
}
}
/// <summary>
/// Enable and create settings files for portable mode
/// Creates the ./Data directory and the `.sm-portable` marker file
@ -280,18 +309,21 @@ public class SettingsManager : ISettingsManager
/// </summary>
public IEnumerable<InstalledPackage> GetOldInstalledPackages()
{
var oldSettingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StabilityMatrix", "settings.json");
var oldSettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StabilityMatrix",
"settings.json"
);
if (!File.Exists(oldSettingsPath))
yield break;
var oldSettingsJson = File.ReadAllText(oldSettingsPath);
var oldSettings = JsonSerializer.Deserialize<Settings>(oldSettingsJson, new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter() }
});
var oldSettings = JsonSerializer.Deserialize<Settings>(
oldSettingsJson,
new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }
);
// Absolute paths are old formats requiring migration
#pragma warning disable CS0618
var oldPackages = oldSettings?.InstalledPackages.Where(package => package.Path != null);
@ -299,7 +331,7 @@ public class SettingsManager : ISettingsManager
if (oldPackages == null)
yield break;
foreach (var package in oldPackages)
{
yield return package;
@ -308,24 +340,27 @@ public class SettingsManager : ISettingsManager
public Guid GetOldActivePackageId()
{
var oldSettingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StabilityMatrix", "settings.json");
var oldSettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StabilityMatrix",
"settings.json"
);
if (!File.Exists(oldSettingsPath))
return default;
var oldSettingsJson = File.ReadAllText(oldSettingsPath);
var oldSettings = JsonSerializer.Deserialize<Settings>(oldSettingsJson, new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter() }
});
var oldSettings = JsonSerializer.Deserialize<Settings>(
oldSettingsJson,
new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }
);
if (oldSettings == null)
return default;
return oldSettings.ActiveInstalledPackageId ?? default;
}
public void AddPathExtension(string pathExtension)
{
Settings.PathExtensions ??= new List<string>();
@ -337,13 +372,14 @@ public class SettingsManager : ISettingsManager
{
return string.Join(";", Settings.PathExtensions ?? new List<string>());
}
/// <summary>
/// Insert path extensions to the front of the PATH environment variable
/// </summary>
public void InsertPathExtensions()
{
if (Settings.PathExtensions == null) return;
if (Settings.PathExtensions == null)
return;
var toInsert = GetPathExtensionsAsString();
// Append the original path, if any
if (originalEnvPath != null)
@ -353,7 +389,7 @@ public class SettingsManager : ISettingsManager
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);
if (package == null || newVersion == null)
@ -361,28 +397,26 @@ public class SettingsManager : ISettingsManager
return;
}
package.PackageVersion = newVersion;
package.DisplayVersion = string.IsNullOrWhiteSpace(package.InstalledBranch)
? newVersion
: $"{package.InstalledBranch}@{newVersion[..7]}";
package.Version = newVersion;
SaveSettings();
}
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.UpdateAvailable = package.UpdateAvailable;
SaveSettings();
}
public List<LaunchOption> GetLaunchArgs(Guid packageId)
{
var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId);
return packageData?.LaunchArgs ?? new();
}
public void SaveLaunchArgs(Guid packageId, List<LaunchOption> launchArgs)
{
var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId);
@ -396,12 +430,17 @@ public class SettingsManager : ISettingsManager
packageData.LaunchArgs = toSave;
SaveSettings();
}
public string? GetActivePackageHost()
{
var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId);
if (package == null) return null;
var hostOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "host");
var package = Settings.InstalledPackages.FirstOrDefault(
x => x.Id == Settings.ActiveInstalledPackageId
);
if (package == null)
return null;
var hostOption = package.LaunchArgs?.FirstOrDefault(
x => x.Name.ToLowerInvariant() == "host"
);
if (hostOption?.OptionValue != null)
{
return hostOption.OptionValue as string;
@ -411,9 +450,14 @@ public class SettingsManager : ISettingsManager
public string? GetActivePackagePort()
{
var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId);
if (package == null) return null;
var portOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "port");
var package = Settings.InstalledPackages.FirstOrDefault(
x => x.Id == Settings.ActiveInstalledPackageId
);
if (package == null)
return null;
var portOption = package.LaunchArgs?.FirstOrDefault(
x => x.Name.ToLowerInvariant() == "port"
);
if (portOption?.OptionValue != null)
{
return portOption.OptionValue as string;
@ -434,11 +478,12 @@ public class SettingsManager : ISettingsManager
}
SaveSettings();
}
public bool IsSharedFolderCategoryVisible(SharedFolderType type)
{
// False for default
if (type == 0) return false;
if (type == 0)
return false;
return Settings.SharedFolderVisibleCategories?.HasFlag(type) ?? false;
}
@ -459,7 +504,7 @@ public class SettingsManager : ISettingsManager
public void SetEulaAccepted()
{
var globalSettings = new GlobalSettings {EulaAccepted = true};
var globalSettings = new GlobalSettings { EulaAccepted = true };
var json = JsonSerializer.Serialize(globalSettings);
File.WriteAllText(GlobalSettingsPath, json);
}
@ -475,11 +520,15 @@ public class SettingsManager : ISettingsManager
var modelHashes = new HashSet<string>();
var sharedModelDirectory = Path.Combine(LibraryDir, "Models");
if (!Directory.Exists(sharedModelDirectory)) return;
var connectedModelJsons = Directory.GetFiles(sharedModelDirectory, "*.cm-info.json",
SearchOption.AllDirectories);
if (!Directory.Exists(sharedModelDirectory))
return;
var connectedModelJsons = Directory.GetFiles(
sharedModelDirectory,
"*.cm-info.json",
SearchOption.AllDirectories
);
foreach (var jsonFile in connectedModelJsons)
{
var json = File.ReadAllText(jsonFile);
@ -492,7 +541,7 @@ public class SettingsManager : ISettingsManager
}
Transaction(s => s.InstalledModelHashes = modelHashes);
sw.Stop();
Logger.Info($"Indexed {modelHashes.Count} checkpoints in {sw.ElapsedMilliseconds}ms");
}
@ -519,9 +568,10 @@ public class SettingsManager : ISettingsManager
var modifiedDefaultSerializerOptions =
SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions();
modifiedDefaultSerializerOptions.Converters.Add(new JsonStringEnumConverter());
Settings =
JsonSerializer.Deserialize<Settings>(settingsContent,
modifiedDefaultSerializerOptions)!;
Settings = JsonSerializer.Deserialize<Settings>(
settingsContent,
modifiedDefaultSerializerOptions
)!;
}
finally
{
@ -538,12 +588,15 @@ public class SettingsManager : ISettingsManager
{
File.Create(SettingsPath).Close();
}
var json = JsonSerializer.Serialize(Settings, new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
});
var json = JsonSerializer.Serialize(
Settings,
new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
}
);
File.WriteAllText(SettingsPath, json);
}
finally
@ -557,4 +610,3 @@ public class SettingsManager : ISettingsManager
return Task.Run(SaveSettings);
}
}

100
StabilityMatrix.Core/Services/TrackedDownloadService.cs

@ -14,18 +14,22 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
private readonly ILogger<TrackedDownloadService> logger;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly ConcurrentDictionary<Guid, (TrackedDownload, FileStream)> downloads = new();
public IEnumerable<TrackedDownload> Downloads => downloads.Values.Select(x => x.Item1);
private readonly ConcurrentDictionary<
Guid,
(TrackedDownload Download, FileStream Stream)
> downloads = new();
public IEnumerable<TrackedDownload> Downloads => downloads.Values.Select(x => x.Download);
/// <inheritdoc />
public event EventHandler<TrackedDownload>? DownloadAdded;
public TrackedDownloadService(
ILogger<TrackedDownloadService> logger,
IDownloadService downloadService,
ISettingsManager settingsManager)
ISettingsManager settingsManager
)
{
this.logger = logger;
this.downloadService = downloadService;
@ -36,12 +40,13 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
{
var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory);
// Ignore if not exist
if (!downloadsDir.Exists) return;
if (!downloadsDir.Exists)
return;
LoadInProgressDownloads(downloadsDir);
});
}
private void OnDownloadAdded(TrackedDownload download)
{
DownloadAdded?.Invoke(this, download);
@ -55,28 +60,32 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
{
// Set download service
download.SetDownloadService(downloadService);
// Create json file
var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory);
downloadsDir.Create();
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
var json = JsonSerializer.Serialize(download);
jsonFileStream.Write(Encoding.UTF8.GetBytes(json));
jsonFileStream.Flush();
// Add to dictionary
downloads.TryAdd(download.Id, (download, jsonFileStream));
// Connect to state changed event to update json file
AttachHandlers(download);
logger.LogDebug("Added download {Download}", download.FileName);
OnDownloadAdded(download);
}
/// <summary>
/// Update the json file for the download.
/// </summary>
@ -85,19 +94,19 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
// Serialize to json
var json = JsonSerializer.Serialize(download);
var jsonBytes = Encoding.UTF8.GetBytes(json);
// Write to file
var (_, fs) = downloads[download.Id];
fs.Seek(0, SeekOrigin.Begin);
fs.Write(jsonBytes);
fs.Flush();
}
private void AttachHandlers(TrackedDownload download)
{
download.ProgressStateChanged += TrackedDownload_OnProgressStateChanged;
}
/// <summary>
/// Handler when the download's state changes
/// </summary>
@ -107,10 +116,10 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
{
return;
}
// Update json file
UpdateJsonForDownload(download);
// If the download is completed, remove it from the dictionary and delete the json file
if (e is ProgressState.Success or ProgressState.Failed or ProgressState.Cancelled)
{
@ -118,11 +127,13 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
{
downloadInfo.Item2.Dispose();
// 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);
}
}
// On successes, run the continuation action
if (e == ProgressState.Success)
{
@ -133,13 +144,13 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
}
}
}
private void LoadInProgressDownloads(DirectoryPath downloadsDir)
{
logger.LogDebug("Indexing in-progress downloads at {DownloadsDir}...", downloadsDir);
var jsonFiles = downloadsDir.Info.EnumerateFiles("*.json", SearchOption.TopDirectoryOnly);
// Add to dictionary, the file name is the guid
foreach (var file in jsonFiles)
{
@ -147,10 +158,10 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
try
{
var fileStream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
// Deserialize json and add to dictionary
var download = JsonSerializer.Deserialize<TrackedDownload>(fileStream)!;
// If the download is marked as working, pause it
if (download.ProgressState == ProgressState.Working)
{
@ -159,23 +170,30 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
else if (download.ProgressState != ProgressState.Inactive)
{
// 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();
// 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();
continue;
}
download.SetDownloadService(downloadService);
downloads.TryAdd(download.Id, (download, fileStream));
AttachHandlers(download);
OnDownloadAdded(download);
logger.LogDebug("Loaded in-progress download {Download}", download.FileName);
}
catch (Exception e)
@ -197,10 +215,10 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
};
AddDownload(download);
return download;
}
/// <summary>
/// Generate a new temp file name that is unique in the given directory.
/// In format of "Unconfirmed {id}.smdownload"
@ -213,14 +231,14 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
for (var i = 0; i < 10; i++)
{
if (tempFile is {Exists: false})
if (tempFile is { Exists: false })
{
return tempFile.Name;
}
var id = Random.Shared.Next(1000000, 9999999);
tempFile = parentDir.JoinFile($"Unconfirmed {id}.smdownload");
}
throw new Exception("Failed to generate a unique temp file name.");
}
@ -241,7 +259,7 @@ public class TrackedDownloadService : ITrackedDownloadService, IDisposable
}
}
}
GC.SuppressFinalize(this);
}
}

1
StabilityMatrix.Core/StabilityMatrix.Core.csproj

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

Loading…
Cancel
Save