Browse Source

Merge branch 'install-refactor' of https://github.com/ionite34/StabilityMatrix into install-refactor

pull/117/head
JT 1 year ago
parent
commit
b1644087c9
  1. 10
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs
  2. 16
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs
  3. 8
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs
  4. 10
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs
  5. 52
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs
  6. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs
  7. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs
  8. 7
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs
  9. 12
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs
  10. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs
  11. 13
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs
  12. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs
  13. 36
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs
  14. 28
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs
  15. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs
  16. 7
      StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs
  17. 11
      StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs
  18. 3
      StabilityMatrix.Avalonia/App.axaml
  19. 128
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  20. 15
      StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
  21. 4
      StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
  22. 6
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
  23. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs
  24. 303
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
  25. 80
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  26. 178
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
  27. 24
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs
  28. 93
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
  29. 49
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  30. 64
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  31. 329
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  32. 182
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  33. 48
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  34. 322
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  35. 18
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs
  36. 16
      StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs
  37. 1
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  38. 3
      StabilityMatrix.Core/Database/ILiteDbContext.cs
  39. 115
      StabilityMatrix.Core/Database/LiteDbContext.cs
  40. 84
      StabilityMatrix.Core/Helper/SharedFolders.cs
  41. 3
      StabilityMatrix.Core/Models/Database/GitCommit.cs
  42. 7
      StabilityMatrix.Core/Models/Database/LocalModelFile.cs
  43. 75
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  44. 91
      StabilityMatrix.Core/Models/InstalledPackage.cs
  45. 13
      StabilityMatrix.Core/Models/InstalledPackageVersion.cs
  46. 6
      StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs
  47. 9
      StabilityMatrix.Core/Models/PackageModification/DownloadPackageVersionStep.cs
  48. 2
      StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs
  49. 6
      StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs
  50. 5
      StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs
  51. 12
      StabilityMatrix.Core/Models/PackageModification/SetupModelFoldersStep.cs
  52. 11
      StabilityMatrix.Core/Models/PackageModification/SetupPrerequisitesStep.cs
  53. 249
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  54. 194
      StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
  55. 148
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  56. 227
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  57. 45
      StabilityMatrix.Core/Models/Packages/DankDiffusion.cs
  58. 153
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  59. 270
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  60. 84
      StabilityMatrix.Core/Models/Packages/UnknownPackage.cs
  61. 387
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  62. 226
      StabilityMatrix.Core/Models/Packages/VoltaML.cs
  63. 2
      StabilityMatrix.Core/Services/IModelIndexService.cs
  64. 11
      StabilityMatrix.Core/Services/ISettingsManager.cs
  65. 71
      StabilityMatrix.Core/Services/ModelIndexService.cs
  66. 259
      StabilityMatrix.Core/Services/SettingsManager.cs
  67. 98
      StabilityMatrix.Core/Services/TrackedDownloadService.cs

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 */
}

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

@ -54,21 +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],
});
Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {message} - {logEvent.Exception?.Message ?? "no error"}");
_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"/>

128
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,13 +169,16 @@ public class BetterContentDialog : ContentDialog
viewModel.SecondaryButtonClick += OnDialogButtonClick;
viewModel.CloseButtonClick += OnDialogButtonClick;
}
else if ((Content as Control)?.DataContext is ContentDialogProgressViewModelBase progressViewModel)
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)
@ -176,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)
@ -191,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)
@ -216,7 +234,7 @@ public class BetterContentDialog : ContentDialog
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
TryBindButtons();
}
@ -242,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)
{
@ -257,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();

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

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

@ -8,17 +8,17 @@ public class ContentDialogProgressViewModelBase : ProgressViewModel
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);

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

@ -20,7 +20,7 @@ public partial class EnvVarsViewModel : ContentDialogViewModelBase
private ObservableCollection<EnvVarKeyPair> envVars = new();
public DataGridCollectionView EnvVarsView => new(EnvVars);
[RelayCommand]
private void AddRow()
{

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

@ -31,55 +31,83 @@ 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 string latestVersionText = string.Empty;
[ObservableProperty] private bool isAdvancedMode;
[ObservableProperty] private bool showDuplicateWarning;
[ObservableProperty] private string? installName;
[ObservableProperty] private SharedFolderMethod selectedSharedFolderMethod;
[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]
private bool showDuplicateWarning;
[ObservableProperty]
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 IsReleaseModeAvailable =>
AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease);
public bool ShowTorchVersionOptions => SelectedTorchVersion != TorchVersion.None;
public ProgressViewModel InstallProgress { get; } = new();
public IEnumerable<IPackageStep> Steps { get; set; }
@ -87,8 +115,10 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
ISettingsManager settingsManager,
IPackageFactory packageFactory,
IPyRunner pyRunner,
IDownloadService downloadService, INotificationService notificationService,
IPrerequisiteHelper prerequisiteHelper)
IDownloadService downloadService,
INotificationService notificationService,
IPrerequisiteHelper prerequisiteHelper
)
{
this.settingsManager = settingsManager;
this.pyRunner = pyRunner;
@ -97,36 +127,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)
{
AvailableVersions =
new ObservableCollection<PackageVersion>(versionOptions.AvailableVersions);
if (!AvailableVersions.Any()) return;
AvailableVersions = new ObservableCollection<PackageVersion>(
versionOptions.AvailableVersions
);
if (!AvailableVersions.Any())
return;
SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease);
}
else
{
AvailableVersions =
new ObservableCollection<PackageVersion>(versionOptions.AvailableBranches);
AvailableVersions = new ObservableCollection<PackageVersion>(
versionOptions.AvailableBranches
);
UpdateSelectedVersionToLatestMain();
}
@ -137,14 +174,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)
{
OnPrimaryButtonClick();
OnPrimaryButtonClick();
}
else
{
@ -160,41 +200,58 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
await dialog.ShowAsync();
}
}
private Task ActuallyInstall()
{
if (string.IsNullOrWhiteSpace(InstallName))
{
notificationService.Show(new Notification("Package name is empty",
"Please enter a name for the package", NotificationType.Error));
notificationService.Show(
new Notification(
"Package name is empty",
"Please enter a name for the package",
NotificationType.Error
)
);
return Task.CompletedTask;
}
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");
downloadOptions.VersionTag =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag;
}
else
{
downloadOptions.CommitHash = SelectedCommit?.Sha ??
throw new NullReferenceException("Selected commit is null");
installedVersion.InstalledBranch = SelectedVersion?.TagName ??
throw new NullReferenceException("Selected version is null");
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 downloadStep = new DownloadPackageVersionStep(
SelectedPackage,
installLocation,
downloadOptions
);
var installStep = new InstallPackageStep(
SelectedPackage,
SelectedTorchVersion,
installLocation
);
var setupModelFoldersStep = new SetupModelFoldersStep(
SelectedPackage,
SelectedSharedFolderMethod,
installLocation
);
var package = new InstalledPackage
{
@ -210,7 +267,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
};
var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, package);
var steps = new List<IPackageStep>
{
prereqStep,
@ -228,7 +285,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
{
OnCloseButtonClick();
}
private void UpdateSelectedVersionToLatestMain()
{
if (AvailableVersions is null)
@ -241,34 +298,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];
SelectedVersion = version;
}
}
[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)
{
@ -284,10 +341,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)
{
@ -302,74 +360,81 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion();
if (Design.IsDesignMode) return;
Dispatcher.UIThread.InvokeAsync(async () =>
{
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;
Logger.Debug($"Loaded release notes for {ReleaseNotes}");
if (!IsReleaseMode)
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();
}
Logger.Debug($"Release mode: {IsReleaseMode}");
var versionOptions = await value.GetAllVersionOptions();
InstallName = SelectedPackage.DisplayName;
LatestVersionText = IsReleaseMode
? $"Latest version: {SelectedVersion.TagName}"
: $"Branch: {SelectedVersion.TagName}";
}).SafeFireAndForget();
AvailableVersions = IsReleaseMode
? new ObservableCollection<PackageVersion>(versionOptions.AvailableVersions)
: new ObservableCollection<PackageVersion>(versionOptions.AvailableBranches);
SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease);
ReleaseNotes = SelectedVersion.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;
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();
}
}
}

80
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,7 +122,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
IsIndeterminate = false;
var libraryDir = settingsManager.LibraryDir;
// get latest version & download & install
SubHeaderText = "Getting latest version...";
var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name);
@ -112,12 +130,11 @@ public partial class OneClickInstallViewModel : ViewModelBase
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;
downloadVersion.VersionTag = versionOptions.AvailableVersions.First().TagName;
installedVersion.InstalledReleaseVersion = downloadVersion.VersionTag;
}
else
@ -125,16 +142,16 @@ public partial class OneClickInstallViewModel : ViewModelBase
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...";
var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
await SelectedPackage.SetupModelFolders(installLocation, recommendedSharedFolderMethod);
var installedPackage = new InstalledPackage
{
DisplayName = SelectedPackage.DisplayName,
@ -151,7 +168,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
st.Settings.InstalledPackages.Add(installedPackage);
st.Settings.ActiveInstalledPackageId = installedPackage.Id;
EventManager.Instance.OnInstalledPackagesChanged();
HeaderText = "Installation complete!";
SubSubHeaderText = string.Empty;
OneClickInstallProgress = 100;
@ -161,15 +178,18 @@ 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 installLocation, DownloadPackageVersionOptions versionOptions)
private async Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions
)
{
SubHeaderText = "Downloading package...";
var progress = new Progress<ProgressReport>(progress =>
{
IsIndeterminate = progress.IsIndeterminate;
@ -186,7 +206,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
{
SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text;
SubHeaderText = "Downloading and installing package requirements...";
var progress = new Progress<ProgressReport>(progress =>
{
SubHeaderText = "Downloading and installing package requirements...";
@ -194,7 +214,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
});
await SelectedPackage.InstallPackage(installLocation, torchVersion, progress);
}
}

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

@ -26,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;
@ -73,24 +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)
{
AvailableVersions =
new ObservableCollection<PackageVersion>(versionOptions.AvailableVersions);
if (!AvailableVersions.Any()) return;
AvailableVersions = new ObservableCollection<PackageVersion>(
versionOptions.AvailableVersions
);
if (!AvailableVersions.Any())
return;
SelectedVersion = AvailableVersions[0];
}
else
{
AvailableVersions =
new ObservableCollection<PackageVersion>(versionOptions.AvailableBranches);
AvailableVersions = new ObservableCollection<PackageVersion>(
versionOptions.AvailableBranches
);
UpdateSelectedVersionToLatestMain();
}
}
@ -99,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)
{
@ -113,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)
{
@ -127,38 +144,42 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
AvailableCommits?.Clear();
return;
}
AvailableVersions?.Clear();
AvailableCommits?.Clear();
AvailableVersionTypes = SelectedBasePackage.AvailableVersionTypes;
if (Design.IsDesignMode) return;
Dispatcher.UIThread.InvokeAsync(async () =>
{
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)
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)
@ -171,14 +192,14 @@ 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)
@ -187,20 +208,19 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
var version = new InstalledPackageVersion();
if (IsReleaseMode)
{
version.InstalledReleaseVersion = SelectedVersion?.TagName ??
throw new NullReferenceException(
"Selected version is null");
version.InstalledReleaseVersion =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
}
else
{
version.InstalledBranch = SelectedVersion?.TagName ??
throw new NullReferenceException(
"Selected version is null");
version.InstalledCommitSha = 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 torchVersion = SelectedBasePackage.GetRecommendedTorchVersion();
var sharedFolderRecommendation = SelectedBasePackage.RecommendedSharedFolderMethod;
var package = new InstalledPackage
@ -215,17 +235,17 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
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
var recommendedSharedFolderMethod = SelectedBasePackage.RecommendedSharedFolderMethod;
await SelectedBasePackage.UpdateModelFolders(PackagePath, recommendedSharedFolderMethod);
settingsManager.Transaction(s => s.InstalledPackages.Add(package));
}
}

24
StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs

@ -16,14 +16,17 @@ public class PackageModificationDialogViewModel : ContentDialogProgressViewModel
private readonly INotificationService notificationService;
private readonly IEnumerable<IPackageStep> steps;
public PackageModificationDialogViewModel(IPackageModificationRunner packageModificationRunner,
INotificationService notificationService, IEnumerable<IPackageStep> steps)
public PackageModificationDialogViewModel(
IPackageModificationRunner packageModificationRunner,
INotificationService notificationService,
IEnumerable<IPackageStep> steps
)
{
this.packageModificationRunner = packageModificationRunner;
this.notificationService = notificationService;
this.steps = steps;
}
public ConsoleViewModel Console { get; } = new();
public override async Task OnLoadedAsync()
@ -34,9 +37,12 @@ public class PackageModificationDialogViewModel : ContentDialogProgressViewModel
packageModificationRunner.ProgressChanged += PackageModificationRunnerOnProgressChanged;
await packageModificationRunner.ExecuteSteps(steps.ToList());
notificationService.Show("Package Install Completed",
"Package install completed successfully.", NotificationType.Success);
notificationService.Show(
"Package Install Completed",
"Package install completed successfully.",
NotificationType.Success
);
OnCloseButtonClick();
}
}
@ -46,14 +52,14 @@ public class PackageModificationDialogViewModel : ContentDialogProgressViewModel
Text = string.IsNullOrWhiteSpace(e.Title)
? packageModificationRunner.CurrentStep?.ProgressTitle
: e.Title;
Value = e.Percentage;
Description = e.Message;
IsIndeterminate = e.IsIndeterminate;
if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Equals("Downloading..."))
if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Equals("Downloading..."))
return;
Console.PostLine(e.Message);
EventManager.Instance.OnScrollToBottomRequested();
}

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

@ -24,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;
@ -61,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
@ -83,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;
@ -110,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)
{
@ -120,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)
@ -136,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;
}

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

@ -24,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;
@ -54,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
@ -93,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;
}

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

@ -24,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;
@ -48,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))
@ -87,20 +100,23 @@ 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, (UnixFileMode) 0x755);
File.SetUnixFileMode(UpdateHelper.ExecutablePath, (UnixFileMode)0x755);
}
UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds...";
await Task.Delay(1000);
UpdateText = "Update complete. Restarting Stability Matrix in 2 seconds...";

329
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,22 +251,24 @@ 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,
activeInstall.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod);
await basePackage.UpdateModelFolders(
packagePath,
activeInstall.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod
);
// Load user launch args from settings and convert to string
var userArgs = settingsManager.GetLaunchArgs(activeInstall.Id);
@ -242,14 +276,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);
RunningPackage = basePackage;
EventManager.Instance.OnRunningPackageStatusChanged(new PackagePair(activeInstall, basePackage));
EventManager.Instance.OnRunningPackageStatusChanged(
new PackagePair(activeInstall, basePackage)
);
}
// Unpacks sitecustomize.py to the target venv
@ -293,12 +329,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,
@ -309,12 +345,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)
@ -324,7 +357,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)
{
@ -362,7 +395,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
ShowConfirmInputPrompt = false;
}
[RelayCommand]
private async Task SendManualInput(string input)
{
@ -370,71 +403,85 @@ 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.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();
}
// Callback for processes
@ -448,7 +495,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");
@ -463,39 +510,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();
@ -514,7 +567,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
RunningPackage = null;
}
await Console.DisposeAsync();
GC.SuppressFinalize(this);
}
}

182
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,10 +80,9 @@ public partial class PackageCardViewModel : ProgressViewModel
else
{
IsUnknownPackage = false;
var basePackage = packageFactory[value.PackageName];
CardImageSource = basePackage?.PreviewImageUri.ToString()
?? Assets.NoImage.ToString();
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,62 +151,74 @@ 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
{
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 torchVersion = Package.PreferredTorchVersion ??
basePackage.GetRecommendedTorchVersion();
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;
@ -198,17 +226,30 @@ public partial class PackageCardViewModel : ProgressViewModel
IsUpdateAvailable = false;
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
{
@ -220,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)
}
};
@ -257,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();
@ -269,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)
@ -290,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)
{

48
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -84,11 +84,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();
}
@ -107,8 +110,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));
@ -135,8 +141,11 @@ public partial class PackageManagerViewModel : PageViewModelBase
if (result == ContentDialogResult.Primary)
{
var steps = viewModel.Steps;
var packageModificationDialogViewModel =
new PackageModificationDialogViewModel(packageModificationRunner, notificationService, steps);
var packageModificationDialogViewModel = new PackageModificationDialogViewModel(
packageModificationRunner,
notificationService,
steps
);
dialog = new BetterContentDialog
{
@ -146,7 +155,10 @@ public partial class PackageManagerViewModel : PageViewModelBase
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new PackageModificationDialog {DataContext = packageModificationDialogViewModel}
Content = new PackageModificationDialog
{
DataContext = packageModificationDialogViewModel
}
};
await dialog.ShowAsync();
@ -156,22 +168,24 @@ public partial class PackageManagerViewModel : PageViewModelBase
}
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))
{

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
}

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

@ -15,32 +15,34 @@ 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");
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;
if (editor?.Document == null)
return;
var line = Math.Max(editor.Document.LineCount - 5, 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);
});

1
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -16,6 +16,7 @@
Width="1100"
Height="750"
Title="Stability Matrix"
FontFamily="San Francisco, Segoe UI, Helvetica Neue, Helvetica, Arial, sans-serif"
x:Class="StabilityMatrix.Avalonia.Views.MainWindow">
<Grid RowDefinitions="Auto,Auto,*">

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

84
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,22 +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
{
var sharedFolderMethod = package.PreferredSharedFolderMethod ??
basePackage.RecommendedSharedFolderMethod;
basePackage.RemoveModelFolderLinks(package.FullPath, sharedFolderMethod)
.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
);
}
}
}
@ -197,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)

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

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

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

91
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -10,45 +10,48 @@ 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.
@ -57,14 +60,16 @@ public class InstalledPackage : IJsonOnDeserialized
{
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.
@ -76,12 +81,13 @@ public class InstalledPackage : IJsonOnDeserialized
#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)
{
@ -105,8 +111,9 @@ public class InstalledPackage : IJsonOnDeserialized
#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);
@ -123,7 +130,8 @@ public class InstalledPackage : IJsonOnDeserialized
#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
@ -136,27 +144,31 @@ public class InstalledPackage : IJsonOnDeserialized
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));
@ -169,7 +181,7 @@ public class InstalledPackage : IJsonOnDeserialized
public static IEqualityComparer<InstalledPackage> Comparer { get; } =
new PropertyComparer<InstalledPackage>(p => p.Id);
protected bool Equals(InstalledPackage other)
{
return Id.Equals(other.Id);
@ -177,9 +189,11 @@ public class InstalledPackage : IJsonOnDeserialized
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()
@ -191,16 +205,15 @@ public class InstalledPackage : IJsonOnDeserialized
public void OnDeserialized()
{
// Handle version migration
if (Version != null)
if (Version != null)
return;
if (string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion))
if (
string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion)
)
{
// release mode
Version = new InstalledPackageVersion
{
InstalledReleaseVersion = PackageVersion
};
Version = new InstalledPackageVersion { InstalledReleaseVersion = PackageVersion };
}
else if (!string.IsNullOrWhiteSpace(PackageVersion))
{

13
StabilityMatrix.Core/Models/InstalledPackageVersion.cs

@ -12,9 +12,12 @@ public class InstalledPackageVersion
public bool IsReleaseMode => string.IsNullOrWhiteSpace(InstalledBranch);
[JsonIgnore]
public string DisplayVersion => (IsReleaseMode
? InstalledReleaseVersion
: string.IsNullOrWhiteSpace(InstalledCommitSha)
? InstalledBranch
: $"{InstalledBranch}@{InstalledCommitSha[..7]}") ?? string.Empty;
public string DisplayVersion =>
(
IsReleaseMode
? InstalledReleaseVersion
: string.IsNullOrWhiteSpace(InstalledCommitSha)
? InstalledBranch
: $"{InstalledBranch}@{InstalledCommitSha[..7]}"
) ?? string.Empty;
}

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

@ -8,8 +8,10 @@ public class AddInstalledPackageStep : IPackageStep
private readonly ISettingsManager settingsManager;
private readonly InstalledPackage newInstalledPackage;
public AddInstalledPackageStep(ISettingsManager settingsManager,
InstalledPackage newInstalledPackage)
public AddInstalledPackageStep(
ISettingsManager settingsManager,
InstalledPackage newInstalledPackage
)
{
this.settingsManager = settingsManager;
this.newInstalledPackage = newInstalledPackage;

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

@ -9,8 +9,11 @@ public class DownloadPackageVersionStep : IPackageStep
private readonly string installPath;
private readonly DownloadPackageVersionOptions downloadOptions;
public DownloadPackageVersionStep(BasePackage package, string installPath,
DownloadPackageVersionOptions downloadOptions)
public DownloadPackageVersionStep(
BasePackage package,
string installPath,
DownloadPackageVersionOptions downloadOptions
)
{
this.package = package;
this.installPath = installPath;
@ -19,6 +22,6 @@ public class DownloadPackageVersionStep : IPackageStep
public Task ExecuteAsync(IProgress<ProgressReport>? progress = null) =>
package.DownloadPackage(installPath, downloadOptions, progress);
public string ProgressTitle => "Downloading package...";
}

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

@ -9,4 +9,4 @@ public interface IPackageModificationRunner
ProgressReport CurrentProgress { get; set; }
IPackageStep? CurrentStep { get; set; }
event EventHandler<ProgressReport>? ProgressChanged;
}
}

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

@ -20,11 +20,7 @@ public class InstallPackageStep : IPackageStep
{
package.ConsoleOutput += (sender, output) =>
{
progress?.Report(new ProgressReport
{
IsIndeterminate = true,
Message = output.Text
});
progress?.Report(new ProgressReport { IsIndeterminate = true, Message = output.Text });
};
await package.InstallPackage(installPath, torchVersion, progress).ConfigureAwait(false);
}

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

@ -11,7 +11,7 @@ public class PackageModificationRunner : IPackageModificationRunner
CurrentProgress = report;
OnProgressChanged(report);
});
IsRunning = true;
foreach (var step in steps)
{
@ -25,7 +25,8 @@ public class PackageModificationRunner : IPackageModificationRunner
public bool IsRunning { get; set; }
public ProgressReport CurrentProgress { get; set; }
public IPackageStep? CurrentStep { get; set; }
public event EventHandler<ProgressReport>? ProgressChanged;
protected virtual void OnProgressChanged(ProgressReport e) => ProgressChanged?.Invoke(this, e);
}

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

@ -9,8 +9,11 @@ public class SetupModelFoldersStep : IPackageStep
private readonly SharedFolderMethod sharedFolderMethod;
private readonly string installPath;
public SetupModelFoldersStep(BasePackage package, SharedFolderMethod sharedFolderMethod,
string installPath)
public SetupModelFoldersStep(
BasePackage package,
SharedFolderMethod sharedFolderMethod,
string installPath
)
{
this.package = package;
this.sharedFolderMethod = sharedFolderMethod;
@ -19,8 +22,9 @@ public class SetupModelFoldersStep : IPackageStep
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{
progress?.Report(new ProgressReport(-1f, "Setting up shared folder links...",
isIndeterminate: true));
progress?.Report(
new ProgressReport(-1f, "Setting up shared folder links...", isIndeterminate: true)
);
await package.SetupModelFolders(installPath, sharedFolderMethod).ConfigureAwait(false);
}

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

@ -19,15 +19,16 @@ public class SetupPrerequisitesStep : IPackageStep
{
// 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));
progress?.Report(
new ProgressReport(-1f, "Installing Python prerequisites...", isIndeterminate: true)
);
await pyRunner.Initialize().ConfigureAwait(false);
if (!PyRunner.PipInstalled)
{
await pyRunner.SetupPip().ConfigureAwait(false);

249
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,114 +28,112 @@ 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 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()
{
new()
{
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new() {"--host"}
},
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.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", "--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
};
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<TorchVersion> AvailableTorchVersions => new[]
{
TorchVersion.Cpu,
TorchVersion.Cuda,
TorchVersion.DirectMl,
TorchVersion.Rocm
};
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()
{
@ -143,8 +141,11 @@ public class A3WebUI : BaseGitPackage
return release.TagName!;
}
public override async Task InstallPackage(string installLocation,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null)
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
@ -173,14 +174,17 @@ public class A3WebUI : BaseGitPackage
}
// 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)
await venvRunner
.PipInstall($"-r requirements_versions.txt", OnConsoleOutput)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Installing Package Requirements",
isIndeterminate: false));
progress?.Report(
new ProgressReport(1f, "Installing Package Requirements", isIndeterminate: false)
);
progress?.Report(new ProgressReport(-1f, "Updating configuration", isIndeterminate: true));
@ -189,14 +193,18 @@ public class A3WebUI : BaseGitPackage
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
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
@ -204,14 +212,14 @@ public class A3WebUI : BaseGitPackage
{
OnConsoleOutput(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);
}
@ -221,16 +229,19 @@ public class A3WebUI : BaseGitPackage
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, OnExit);
}
private async Task InstallRocmTorch(PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null)
private async Task InstallRocmTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm",
isIndeterminate: true));
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
await venvRunner.PipInstall("--upgrade pip wheel", OnConsoleOutput)
.ConfigureAwait(false);
await venvRunner.PipInstall("--upgrade pip wheel", OnConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm511, OnConsoleOutput)
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm511, OnConsoleOutput)
.ConfigureAwait(false);
}
}

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

@ -21,13 +21,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>
@ -47,21 +47,23 @@ public abstract class BaseGitPackage : BasePackage
if (!string.IsNullOrWhiteSpace(versionOptions.VersionTag))
{
return
$"https://api.github.com/repos/{Author}/{Name}/zipball/{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}";
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;
@ -71,51 +73,53 @@ 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<PackageVersionOptions> GetAllVersionOptions()
{
var packageVersionOptions = new PackageVersionOptions();
var allReleases = await GetAllReleases().ConfigureAwait(false);
var releasesList = allReleases.ToList();
if (releasesList.Any())
{
packageVersionOptions.AvailableVersions = releasesList.Select(r =>
new PackageVersion
{
TagName = r.TagName!,
ReleaseNotesMarkdown = r.Body,
IsPrerelease = r.Prerelease
});
packageVersionOptions.AvailableVersions = releasesList.Select(
r =>
new PackageVersion
{
TagName = r.TagName!,
ReleaseNotesMarkdown = r.Body,
IsPrerelease = r.Prerelease
}
);
}
// Branch mode
var allBranches = await GetAllBranches().ConfigureAwait(false);
packageVersionOptions.AvailableBranches = 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;
}
@ -125,9 +129,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)
@ -140,24 +145,25 @@ 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 DownloadPackage(string installLocation,
DownloadPackageVersionOptions versionOptions, IProgress<ProgressReport>? progress = null)
public override async Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null
)
{
var downloadUrl = GetDownloadUrl(versionOptions);
@ -173,8 +179,11 @@ public abstract class BaseGitPackage : BasePackage
progress?.Report(new ProgressReport(100, message: "Download Complete"));
}
public override async Task InstallPackage(string installLocation,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null)
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
)
{
await UnzipPackage(installLocation, progress).ConfigureAwait(false);
File.Delete(DownloadLocation);
@ -186,7 +195,7 @@ public abstract class BaseGitPackage : BasePackage
var zipDirName = string.Empty;
var totalEntries = zip.Entries.Count;
var currentEntry = 0;
foreach (var entry in zip.Entries)
{
currentEntry++;
@ -196,20 +205,26 @@ 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)
)
);
}
return Task.CompletedTask;
@ -232,8 +247,9 @@ public abstract class BaseGitPackage : BasePackage
return UpdateAvailable;
}
var allCommits = (await GetAllCommits(currentVersion.InstalledBranch)
.ConfigureAwait(false))?.ToList();
var allCommits = (
await GetAllCommits(currentVersion.InstalledBranch).ConfigureAwait(false)
)?.ToList();
if (allCommits == null || !allCommits.Any())
{
Logger.Warn("No commits found for {Package}", package.PackageName);
@ -241,7 +257,6 @@ public abstract class BaseGitPackage : BasePackage
}
var latestCommitHash = allCommits.First().Sha;
return latestCommitHash != currentVersion.InstalledCommitSha;
}
catch (ApiException e)
{
@ -250,34 +265,37 @@ public abstract class BaseGitPackage : BasePackage
}
}
public override async Task<InstalledPackageVersion> Update(InstalledPackage installedPackage,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null,
bool includePrerelease = false)
public override async Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false
)
{
if (installedPackage.Version == null) throw new NullReferenceException("Version is null");
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(installedPackage.FullPath,
new DownloadPackageVersionOptions {VersionTag = latestRelease.TagName},
progress)
await DownloadPackage(
installedPackage.FullPath,
new DownloadPackageVersionOptions { VersionTag = latestRelease.TagName },
progress
)
.ConfigureAwait(false);
await InstallPackage(installedPackage.FullPath, torchVersion, progress)
.ConfigureAwait(false);
return new InstalledPackageVersion
{
InstalledReleaseVersion = latestRelease.TagName
};
return new InstalledPackageVersion { InstalledReleaseVersion = latestRelease.TagName };
}
// Commit mode
var allCommits = await GetAllCommits(
installedPackage.Version.InstalledBranch).ConfigureAwait(false);
var allCommits = await GetAllCommits(installedPackage.Version.InstalledBranch)
.ConfigureAwait(false);
var latestCommit = allCommits?.First();
if (latestCommit is null || string.IsNullOrEmpty(latestCommit.Sha))
@ -285,8 +303,11 @@ public abstract class BaseGitPackage : BasePackage
throw new Exception("No commits found for branch");
}
await DownloadPackage(installedPackage.FullPath,
new DownloadPackageVersionOptions {CommitHash = latestCommit.Sha}, progress)
await DownloadPackage(
installedPackage.FullPath,
new DownloadPackageVersionOptions { CommitHash = latestCommit.Sha },
progress
)
.ConfigureAwait(false);
await InstallPackage(installedPackage.FullPath, torchVersion, progress)
.ConfigureAwait(false);
@ -298,29 +319,40 @@ public abstract class BaseGitPackage : BasePackage
};
}
public override Task SetupModelFolders(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod)
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
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,
SharedFolderMethod sharedFolderMethod)
public override async Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
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, SharedFolderMethod sharedFolderMethod)
public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
if (SharedFolders is not null && sharedFolderMethod == SharedFolderMethod.Symlink)
{
@ -340,7 +372,7 @@ public abstract class BaseGitPackage : BasePackage
}
process.StandardInput.WriteLine(input);
}
public virtual async Task SendInputAsync(string input)
{
var process = VenvRunner?.Process;
@ -361,7 +393,7 @@ public abstract class BaseGitPackage : BasePackage
VenvRunner = null;
}
}
/// <inheritdoc />
public override async Task WaitForShutdown()
{

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

@ -21,52 +21,68 @@ 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 DownloadPackage(string installLocation, DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress1);
public abstract Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress1
);
public abstract Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
);
public abstract Task InstallPackage(string installLocation, TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null);
public abstract Task RunPackage(string installedPackagePath, string command, string arguments);
public abstract Task<bool> CheckForUpdates(InstalledPackage package);
public abstract Task<InstalledPackageVersion> Update(InstalledPackage installedPackage,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null,
bool includePrerelease = false);
public virtual IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods => new[]
{
SharedFolderMethod.Symlink,
SharedFolderMethod.Configuration,
SharedFolderMethod.None
};
public abstract Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false
);
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 SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task UpdateModelFolders(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod);
public abstract Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod);
public abstract Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract IEnumerable<TorchVersion> AvailableTorchVersions { get; }
@ -77,7 +93,7 @@ public abstract class BasePackage
{
return AvailableTorchVersions.First();
}
if (HardwareHelper.HasNvidiaGpu() && AvailableTorchVersions.Contains(TorchVersion.Cuda))
{
return TorchVersion.Cuda;
@ -88,15 +104,17 @@ public abstract class BasePackage
return TorchVersion.Rocm;
}
if (HardwareHelper.PreferDirectML() &&
AvailableTorchVersions.Contains(TorchVersion.DirectMl))
if (
HardwareHelper.PreferDirectML()
&& AvailableTorchVersions.Contains(TorchVersion.DirectMl)
)
{
return TorchVersion.DirectMl;
}
return TorchVersion.Cpu;
}
/// <summary>
/// Shuts down the subprocess, canceling any pending streams.
/// </summary>
@ -110,16 +128,20 @@ public abstract class BasePackage
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<PackageVersionOptions> GetAllVersionOptions();
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10);
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(
string branch,
int page = 1,
int perPage = 10
);
public abstract Task<IEnumerable<Branch>> GetAllBranches();
public abstract Task<IEnumerable<Release>> GetAllReleases();
public event EventHandler<ProcessOutput>? ConsoleOutput;
@ -127,44 +149,56 @@ public abstract class BasePackage
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)
public virtual PackageVersionType AvailableVersionTypes =>
ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
protected async Task InstallCudaTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CUDA",
isIndeterminate: true));
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true)
);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput)
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)
protected async Task InstallDirectMlTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for DirectML",
isIndeterminate: true));
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true)
);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, OnConsoleOutput)
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, OnConsoleOutput)
.ConfigureAwait(false);
}
protected async Task InstallCpuTorch(PyVenvRunner venvRunner, IProgress<ProgressReport>? progress = null)
protected async Task InstallCpuTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CPU",
isIndeterminate: true));
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true)
);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput)
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput)
.ConfigureAwait(false);
}
}

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

@ -21,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";
@ -32,77 +32,82 @@ public class ComfyUI : BaseGitPackage
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 IEnumerable<TorchVersion> AvailableTorchVersions => new[]
{
TorchVersion.Cpu,
TorchVersion.Cuda,
TorchVersion.DirectMl,
TorchVersion.Rocm
};
public override async Task InstallPackage(string installLocation,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null)
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
@ -132,17 +137,21 @@ public class ComfyUI : BaseGitPackage
}
// Install requirements file
progress?.Report(new ProgressReport(-1, "Installing Package Requirements",
isIndeterminate: true));
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));
progress?.Report(
new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)
);
}
private async Task AutoDetectAndInstallTorch(PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null)
private async Task AutoDetectAndInstallTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
{
var gpus = HardwareHelper.IterGpuInfo().ToList();
if (gpus.Any(g => g.IsNvidia))
@ -163,14 +172,18 @@ public class ComfyUI : BaseGitPackage
}
}
public override async Task RunPackage(string installedPackagePath, string command, string arguments)
public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase))
{
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
@ -191,14 +204,13 @@ 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,
SharedFolderMethod sharedFolderMethod)
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
switch (sharedFolderMethod)
{
@ -223,19 +235,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");
@ -243,7 +258,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();
@ -253,33 +268,39 @@ public class ComfyUI : BaseGitPackage
return Task.CompletedTask;
}
public override Task UpdateModelFolders(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod) =>
SetupModelFolders(installDirectory, sharedFolderMethod);
public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) => SetupModelFolders(installDirectory, sharedFolderMethod);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod)
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),
SharedFolderMethod.Symlink
=> base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
_ => Task.CompletedTask
};
}
private async Task InstallRocmTorch(PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null)
private async Task InstallRocmTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm",
isIndeterminate: true));
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
await venvRunner.PipInstall("--upgrade pip wheel", OnConsoleOutput)
.ConfigureAwait(false);
await venvRunner.PipInstall("--upgrade pip wheel", OnConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, OnConsoleOutput)
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, OnConsoleOutput)
.ConfigureAwait(false);
}

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

@ -7,28 +7,26 @@ 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 SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Symlink;
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)
@ -36,20 +34,26 @@ public class DankDiffusion : BaseGitPackage
throw new NotImplementedException();
}
public override Task SetupModelFolders(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod)
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
public override Task UpdateModelFolders(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod)
public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod)
public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
@ -57,6 +61,7 @@ public class DankDiffusion : BaseGitPackage
public override IEnumerable<TorchVersion> AvailableTorchVersions { get; }
public override List<LaunchOptionDefinition> LaunchOptions { get; }
public override Task<string> GetLatestVersion()
{
throw new NotImplementedException();

153
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,76 +30,76 @@ 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()
{
new LaunchOptionDefinition
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
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
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()
{
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
};
[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 IEnumerable<TorchVersion> AvailableTorchVersions => new[]
{
TorchVersion.Cpu,
TorchVersion.Cuda,
TorchVersion.Rocm
};
public override async Task<string> GetLatestVersion()
{
var release = await GetLatestRelease().ConfigureAwait(false);
return release.TagName!;
}
public override async Task InstallPackage(string installLocation,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null)
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true).ConfigureAwait(false);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true));
@ -120,22 +122,30 @@ public class Fooocus : BaseGitPackage
await venvRunner
.PipInstall(
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}",
OnConsoleOutput).ConfigureAwait(false);
OnConsoleOutput
)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing requirements...",
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
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase))
{
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
@ -156,9 +166,6 @@ public class Fooocus : BaseGitPackage
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner?.RunDetached(
args.TrimEnd(),
HandleConsoleOutput,
HandleExit);
VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
}
}

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

@ -21,114 +21,112 @@ 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 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>
{
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
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>
[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 IEnumerable<TorchVersion> AvailableTorchVersions => new[]
{
TorchVersion.Cpu,
TorchVersion.Cuda,
TorchVersion.Rocm,
TorchVersion.Mps
};
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm, TorchVersion.Mps };
public override TorchVersion GetRecommendedTorchVersion()
{
@ -140,14 +138,20 @@ public class InvokeAI : BaseGitPackage
return base.GetRecommendedTorchVersion();
}
public override Task DownloadPackage(string installLocation,
DownloadPackageVersionOptions downloadOptions, IProgress<ProgressReport>? progress = null)
public override Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions downloadOptions,
IProgress<ProgressReport>? progress = null
)
{
return Task.CompletedTask;
}
public override async Task InstallPackage(string installLocation, TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null)
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
)
{
// Setup venv
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
@ -169,13 +173,13 @@ public class InvokeAI : BaseGitPackage
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";
@ -190,15 +194,23 @@ public class InvokeAI : BaseGitPackage
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
)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false));
}
public override async Task<InstalledPackageVersion> Update(InstalledPackage installedPackage,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null,
bool includePrerelease = false)
public override async Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false
)
{
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
@ -206,8 +218,10 @@ public class InvokeAI : BaseGitPackage
{
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);
@ -251,10 +265,7 @@ public class InvokeAI : BaseGitPackage
progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false));
return isReleaseMode
? new InstalledPackageVersion
{
InstalledReleaseVersion = latestVersion
}
? new InstalledPackageVersion { InstalledReleaseVersion = latestVersion }
: new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.InstalledBranch,
@ -262,15 +273,20 @@ public class InvokeAI : BaseGitPackage
};
}
public override Task
RunPackage(string installedPackagePath, string command, string arguments) =>
RunInvokeCommand(installedPackagePath, command, arguments, true);
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)
private async Task<string> GetUpdateVersion(
InstalledPackage installedPackage,
bool includePrerelease = false
)
{
if (installedPackage.Version == null)
throw new NullReferenceException("Installed package version is null");
if (installedPackage.Version.IsReleaseMode)
{
var releases = await GetAllReleases().ConfigureAwait(false);
@ -284,8 +300,12 @@ public class InvokeAI : BaseGitPackage
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
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
@ -304,12 +324,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
@ -326,28 +346,30 @@ public class InvokeAI : BaseGitPackage
{
OnConsoleOutput(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(new ProcessOutput { Text = result.StandardOutput });
}
}

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

@ -14,30 +14,32 @@ 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",
};
public override SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Symlink;
public override Task DownloadPackage(string installLocation, DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress1)
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(string installLocation, TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null)
public override Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
)
{
throw new NotImplementedException();
}
@ -48,33 +50,34 @@ public class UnknownPackage : BasePackage
}
/// <inheritdoc />
public override Task SetupModelFolders(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod)
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task UpdateModelFolders(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod)
public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod)
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
};
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cuda, TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl };
/// <inheritdoc />
public override void Shutdown()
@ -95,28 +98,39 @@ public class UnknownPackage : BasePackage
}
/// <inheritdoc />
public override Task<InstalledPackageVersion> Update(InstalledPackage installedPackage,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null,
bool includePrerelease = false)
public override Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false
)
{
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<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>());
}

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

@ -17,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";
@ -30,129 +30,133 @@ public class VladAutomatic : BaseGitPackage
public override Uri PreviewImageUri =>
new("https://github.com/vladmandic/automatic/raw/master/html/black-orange.jpg");
public override bool ShouldIgnoreReleases => true;
public override SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Symlink;
public override IEnumerable<TorchVersion> AvailableTorchVersions => new[]
{
TorchVersion.Cpu,
TorchVersion.Rocm,
TorchVersion.DirectMl,
TorchVersion.Cuda
};
public VladAutomatic(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper) :
base(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
}
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 = "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 InstallPackage(string installLocation, TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null)
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, "Installing package...", isIndeterminate: true));
// Setup venv
@ -166,11 +170,13 @@ public class VladAutomatic : BaseGitPackage
{
// Run initial install
case TorchVersion.Cuda:
await venvRunner.CustomInstall("launch.py --use-cuda --debug --test", OnConsoleOutput)
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)
await venvRunner
.CustomInstall("launch.py --use-rocm --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
break;
case TorchVersion.DirectMl:
@ -180,7 +186,8 @@ public class VladAutomatic : BaseGitPackage
break;
default:
// CPU
await venvRunner.CustomInstall("launch.py --debug --test", OnConsoleOutput)
await venvRunner
.CustomInstall("launch.py --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
break;
}
@ -188,11 +195,20 @@ public class VladAutomatic : BaseGitPackage
progress?.Report(new ProgressReport(1f, isIndeterminate: false));
}
public override async Task DownloadPackage(string installLocation,
DownloadPackageVersionOptions downloadOptions, 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);
installDir.Create();
@ -200,22 +216,38 @@ public class VladAutomatic : BaseGitPackage
if (!string.IsNullOrWhiteSpace(downloadOptions.CommitHash))
{
await PrerequisiteHelper
.RunGit(installDir.Parent ?? "", "clone", "https://github.com/vladmandic/automatic",
installDir.Name).ConfigureAwait(false);
.RunGit(
installDir.Parent ?? "",
"clone",
"https://github.com/vladmandic/automatic",
installDir.Name
)
.ConfigureAwait(false);
await PrerequisiteHelper.RunGit(installLocation, "checkout", downloadOptions.CommitHash)
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)
.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
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
@ -245,35 +277,44 @@ public class VladAutomatic : BaseGitPackage
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
}
public override async Task<InstalledPackageVersion> Update(InstalledPackage installedPackage,
TorchVersion torchVersion, IProgress<ProgressReport>? progress = null,
bool includePrerelease = false)
public override async Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false
)
{
if (installedPackage.Version 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.Version.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 = installedPackage.FullPath!;
venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables;
await venvRunner.CustomInstall("launch.py --upgrade --test", OnConsoleOutput)
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 new InstalledPackageVersion
{
@ -287,9 +328,14 @@ 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 new InstalledPackageVersion
@ -298,7 +344,10 @@ public class VladAutomatic : BaseGitPackage
};
}
public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod)
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{
switch (sharedFolderMethod)
{
@ -335,38 +384,56 @@ public class VladAutomatic : BaseGitPackage
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["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["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
});
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 UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) => SetupModelFolders(installDirectory, sharedFolderMethod);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod) =>
public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) =>
sharedFolderMethod switch
{
SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory,
sharedFolderMethod),
SharedFolderMethod.Symlink
=> base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
SharedFolderMethod.None => Task.CompletedTask,
_ => Task.CompletedTask
};

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

@ -14,128 +14,130 @@ 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"},
};
public override SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Symlink;
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
[SharedFolderType.StableDiffusion] = new[] { "data/models" },
[SharedFolderType.Lora] = new[] { "data/lora" },
[SharedFolderType.TextualInversion] = new[] { "data/textual-inversion" },
};
public override IEnumerable<TorchVersion> AvailableTorchVersions => new[] {TorchVersion.None};
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable<TorchVersion> AvailableTorchVersions => new[] { TorchVersion.None };
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.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>
{
new()
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{
Name = "Log Level",
Type = LaunchOptionType.Bool,
DefaultValue = "--log-level INFO",
Options =
new()
{
"--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 =
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()
{
"--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 = "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(string installLocation, TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null)
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
@ -147,24 +149,30 @@ public class VoltaML : BaseGitPackage
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)
.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
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
var foundIndicator = false;
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
@ -178,17 +186,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);
}
}

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>

11
StabilityMatrix.Core/Services/ISettingsManager.cs

@ -23,7 +23,7 @@ public interface ISettingsManager
/// Will fire instantly if it is already set.
/// </summary>
void RegisterOnLibraryDirSet(Action<string> handler);
/// <inheritdoc />
SettingsTransaction BeginTransaction();
@ -35,14 +35,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

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 />

259
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,10 @@ 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 Settings Settings { get; private set; } = new();
public event EventHandler<string>? LibraryDirChanged;
public event EventHandler<string>? LibraryDirChanged;
public event EventHandler<PropertyChangedEventArgs>? SettingsPropertyChanged;
/// <inheritdoc />
@ -66,7 +72,7 @@ public class SettingsManager : ISettingsManager
}
LibraryDirChanged += Handler;
return;
void Handler(object? sender, string dir)
@ -75,17 +81,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 +110,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 +231,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 +277,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 +308,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 +330,7 @@ public class SettingsManager : ISettingsManager
if (oldPackages == null)
yield break;
foreach (var package in oldPackages)
{
yield return package;
@ -308,24 +339,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 +371,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)
@ -364,21 +399,23 @@ public class SettingsManager : ISettingsManager
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);
@ -392,12 +429,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;
@ -407,9 +449,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;
@ -430,11 +477,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;
}
@ -455,7 +503,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);
}
@ -471,11 +519,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);
@ -488,7 +540,7 @@ public class SettingsManager : ISettingsManager
}
Transaction(s => s.InstalledModelHashes = modelHashes);
sw.Stop();
Logger.Info($"Indexed {modelHashes.Count} checkpoints in {sw.ElapsedMilliseconds}ms");
}
@ -515,9 +567,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
{
@ -534,12 +587,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
@ -553,4 +609,3 @@ public class SettingsManager : ISettingsManager
return Task.Run(SaveSettings);
}
}

98
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 Download, FileStream Stream)> downloads = new();
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);
}
}

Loading…
Cancel
Save