Browse Source

Merge pull request #117 from LykosAI/main

pull/118/head
Ionite 1 year ago committed by GitHub
parent
commit
4f71877ddb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .husky/task-runner.json
  2. 18
      CHANGELOG.md
  3. 6
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs
  4. 16
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs
  5. 8
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs
  6. 10
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs
  7. 28
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs
  8. 5
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs
  9. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs
  10. 13
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs
  11. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs
  12. 16
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs
  13. 22
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs
  14. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs
  15. 3
      StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs
  16. 9
      StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs
  17. 3
      StabilityMatrix.Avalonia/App.axaml
  18. 14
      StabilityMatrix.Avalonia/App.axaml.cs
  19. 127
      StabilityMatrix.Avalonia/Assets.cs
  20. 72
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  21. 164
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  22. 15
      StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
  23. 4
      StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
  24. 549
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  25. 33
      StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx
  26. 183
      StabilityMatrix.Avalonia/Languages/Resources.resx
  27. 60
      StabilityMatrix.Avalonia/Program.cs
  28. 11
      StabilityMatrix.Avalonia/ViewModels/Base/ConsoleProgressViewModel.cs
  29. 26
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
  30. 3
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs
  31. 13
      StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs
  32. 288
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  33. 138
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  34. 249
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  35. 172
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  36. 109
      StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
  37. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs
  38. 367
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
  39. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
  40. 97
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  41. 121
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
  42. 48
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
  43. 46
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  44. 54
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  45. 172
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  46. 28
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  47. 137
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  48. 49
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  49. 2
      StabilityMatrix.Avalonia/ViewModels/Progress/DownloadProgressItemViewModel.cs
  50. 78
      StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
  51. 6
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressItemViewModel.cs
  52. 56
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs
  53. 218
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  54. 31
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  55. 85
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  56. 5
      StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml
  57. 378
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  58. 5
      StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml
  59. 57
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
  60. 51
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs
  61. 13
      StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml
  62. 11
      StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
  63. 3
      StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml
  64. 11
      StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml
  65. 6
      StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs
  66. 9
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  67. 7
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  68. 69
      StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml
  69. 1
      StabilityMatrix.Core/Database/ILiteDbContext.cs
  70. 77
      StabilityMatrix.Core/Database/LiteDbContext.cs
  71. 40
      StabilityMatrix.Core/Helper/EventManager.cs
  72. 65
      StabilityMatrix.Core/Helper/SharedFolders.cs
  73. 7
      StabilityMatrix.Core/Models/Database/GitCommit.cs
  74. 3
      StabilityMatrix.Core/Models/Database/LocalModelFile.cs
  75. 8
      StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs
  76. 29
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  77. 72
      StabilityMatrix.Core/Models/InstalledPackage.cs
  78. 23
      StabilityMatrix.Core/Models/InstalledPackageVersion.cs
  79. 33
      StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs
  80. 27
      StabilityMatrix.Core/Models/PackageModification/DownloadPackageVersionStep.cs
  81. 14
      StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs
  82. 33
      StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs
  83. 43
      StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs
  84. 9
      StabilityMatrix.Core/Models/PackageModification/PackageStep.cs
  85. 24
      StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs
  86. 32
      StabilityMatrix.Core/Models/PackageModification/SetupModelFoldersStep.cs
  87. 44
      StabilityMatrix.Core/Models/PackageModification/SetupPrerequisitesStep.cs
  88. 1
      StabilityMatrix.Core/Models/PackageVersion.cs
  89. 138
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  90. 228
      StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
  91. 165
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  92. 206
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  93. 49
      StabilityMatrix.Core/Models/Packages/DankDiffusion.cs
  94. 111
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  95. 175
      StabilityMatrix.Core/Models/Packages/FooocusMre.cs
  96. 215
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  97. 7
      StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs
  98. 75
      StabilityMatrix.Core/Models/Packages/UnknownPackage.cs
  99. 312
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  100. 58
      StabilityMatrix.Core/Models/Packages/VoltaML.cs
  101. Some files were not shown because too many files have changed in this diff Show More

2
.husky/task-runner.json

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

18
CHANGELOG.md

@ -5,6 +5,24 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.4.0
### Added
- New installable Package - [Fooocus-MRE](https://github.com/MoonRide303/Fooocus-MRE)
- Added toggle to show connected model images in the Checkpoints tab
- Added "Find Connected Metadata" option to the context menu of Checkpoint Folders in the Checkpoints tab to connect models that don't have any metadata
### Changed
- Revamped package installer
- Added "advanced options" section for commit, shared folder method, and pytorch options
- Can be run in the background
- Shows progress in the Downloads tab
- Even more performance improvements for loading and searching the Checkpoints page
### Fixed
- Fixed [#97](https://github.com/LykosAI/StabilityMatrix/issues/97) - Codeformer folder should now get linked correctly
- Fixed [#106](https://github.com/LykosAI/StabilityMatrix/issues/106) - ComfyUI should now install correctly on Windows machines with an AMD GPU using DirectML
- Fixed [#107](https://github.com/LykosAI/StabilityMatrix/issues/107) - Added `--autolaunch` option to SD.Next
- Fixed [#110](https://github.com/LykosAI/StabilityMatrix/issues/110) - Model Browser should properly navigate to the next page of Installed models
- Installed tag on model browser should now show for connected models imported via drag & drop
## v2.3.4
### Fixed
- Fixed [#108](https://github.com/LykosAI/StabilityMatrix/issues/108) - (Linux) Fixed permission error on updates [#103](https://github.com/LykosAI/StabilityMatrix/pull/103)

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

@ -8,8 +8,7 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Controls;
public partial class LogViewerControl : UserControl
{
public LogViewerControl()
=> InitializeComponent();
public LogViewerControl() => InitializeComponent();
private ILogDataStoreImpl? vm;
private LogModel? item;
@ -46,7 +45,8 @@ public partial class LogViewerControl : UserControl
{
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;

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

@ -9,24 +9,13 @@ public class DataStoreLoggerConfiguration
public EventId EventId { get; set; }
public Dictionary<LogLevel, LogEntryColor> Colors { get; } = new()
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.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,
@ -37,10 +26,7 @@ public class DataStoreLoggerConfiguration
Foreground = Color.White,
Background = Color.Red
},
[LogLevel.None] = new LogEntryColor
{
Foreground = Color.Magenta
}
[LogLevel.None] = new LogEntryColor { Foreground = Color.Magenta }
};
#endregion

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

@ -4,9 +4,7 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
public class LogEntryColor
{
public 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;
}

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

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

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

@ -5,9 +5,14 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
public class ObservableObject : INotifyPropertyChanged
{
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 */
}

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

@ -54,21 +54,27 @@ public class DataStoreLoggerTarget : TargetWithLayout
}
// add log entry
_dataStore?.AddEntry(new LogModel
_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,
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 : ""),
Exception =
logEvent.Exception?.Message ?? (logLevel == MsLogLevel.Error ? message : ""),
Color = _config!.Colors[logLevel],
});
}
);
Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {message} - {logEvent.Exception?.Message ?? "no error"}");
Debug.WriteLine(
$"--- [{logLevel.ToString()[..3]}] {message} - {logEvent.Exception?.Message ?? "no error"}"
);
}
#endregion

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

@ -15,7 +15,7 @@ public static class ServicesExtension
{
public static IServiceCollection AddLogViewer(this IServiceCollection services)
{
services.AddSingleton<ILogDataStore, LogDataStore>();
services.AddSingleton<ILogDataStore>(Core.Logging.LogDataStore.Instance);
services.AddSingleton<LogViewerControlViewModel>();
return services;
@ -23,7 +23,8 @@ public static class ServicesExtension
public static IServiceCollection AddLogViewer(
this IServiceCollection services,
Action<DataStoreLoggerConfiguration> configure)
Action<DataStoreLoggerConfiguration> configure
)
{
services.AddSingleton<ILogDataStore>(Core.Logging.LogDataStore.Instance);
services.AddSingleton<LogViewerControlViewModel>();
@ -32,13 +33,18 @@ public static class ServicesExtension
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
}

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

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

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

@ -17,12 +17,17 @@ public partial class LogWindow : Window
return Attach(root, serviceProvider, new KeyGesture(Key.F11));
}
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"/>

14
StabilityMatrix.Avalonia/App.axaml.cs

@ -47,6 +47,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Api;
@ -57,6 +58,7 @@ using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Configs;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Python;
@ -340,11 +342,12 @@ public sealed class App : Application
internal static void ConfigurePackages(IServiceCollection services)
{
services.AddSingleton<BasePackage, A3WebUI>();
services.AddSingleton<BasePackage, Fooocus>();
services.AddSingleton<BasePackage, VladAutomatic>();
services.AddSingleton<BasePackage, InvokeAI>();
services.AddSingleton<BasePackage, ComfyUI>();
services.AddSingleton<BasePackage, VoltaML>();
services.AddSingleton<BasePackage, InvokeAI>();
services.AddSingleton<BasePackage, Fooocus>();
services.AddSingleton<BasePackage, FooocusMre>();
}
private static IServiceCollection ConfigureServices()
@ -370,6 +373,7 @@ public sealed class App : Application
services.AddSingleton<IUpdateHelper, UpdateHelper>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IModelIndexService, ModelIndexService>();
services.AddTransient<IPackageModificationRunner, PackageModificationRunner>();
services.AddSingleton<ITrackedDownloadService, TrackedDownloadService>();
services.AddSingleton<IDisposable>(
@ -417,7 +421,8 @@ public sealed class App : Application
// if (string.IsNullOrWhiteSpace(githubApiKey))
// return client;
//
// client.Credentials = new Credentials(githubApiKey);
// client.Credentials =
// new Credentials("");
return client;
});
@ -601,6 +606,9 @@ public sealed class App : Application
builder.ForLogger("System.*").WriteToNil(NLog.LogLevel.Warn);
builder.ForLogger("Microsoft.*").WriteToNil(NLog.LogLevel.Warn);
builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn);
builder
.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel")
.WriteToNil(NLog.LogLevel.Debug);
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget);
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget);

127
StabilityMatrix.Avalonia/Assets.cs

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

72
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -20,7 +20,9 @@ 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
{
@ -30,7 +32,9 @@ public class BetterContentDialog : ContentDialog
[NotNull]
protected static readonly MethodInfo? HideCoreMethod = typeof(ContentDialog).GetMethod(
"HideCore", BindingFlags.Instance | BindingFlags.NonPublic);
"HideCore",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected void HideCore()
{
@ -40,7 +44,9 @@ public class BetterContentDialog : ContentDialog
// 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
{
@ -50,7 +56,9 @@ public class BetterContentDialog : ContentDialog
[NotNull]
protected static readonly FieldInfo? SecondaryButtonField = typeof(ContentDialog).GetField(
"_secondaryButton", BindingFlags.Instance | BindingFlags.NonPublic);
"_secondaryButton",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected Button? SecondaryButton
{
@ -60,7 +68,9 @@ public class BetterContentDialog : ContentDialog
[NotNull]
protected static readonly FieldInfo? CloseButtonField = typeof(ContentDialog).GetField(
"_closeButton", BindingFlags.Instance | BindingFlags.NonPublic);
"_closeButton",
BindingFlags.Instance | BindingFlags.NonPublic
);
protected Button? CloseButton
{
@ -87,8 +97,10 @@ public class BetterContentDialog : ContentDialog
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,8 +120,8 @@ 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
{
@ -115,8 +129,8 @@ public class BetterContentDialog : ContentDialog
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,8 +147,8 @@ 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
{
@ -142,7 +156,6 @@ public class BetterContentDialog : ContentDialog
set => SetValue(ContentMarginProperty, value);
}
public BetterContentDialog()
{
AddHandler(LoadedEvent, OnLoaded);
@ -156,6 +169,15 @@ public class BetterContentDialog : ContentDialog
viewModel.SecondaryButtonClick += OnDialogButtonClick;
viewModel.CloseButtonClick += OnDialogButtonClick;
}
else if (
(Content as Control)?.DataContext
is ContentDialogProgressViewModelBase progressViewModel
)
{
progressViewModel.PrimaryButtonClick += OnDialogButtonClick;
progressViewModel.SecondaryButtonClick += OnDialogButtonClick;
progressViewModel.CloseButtonClick += OnDialogButtonClick;
}
// If commands provided, bind OnCanExecuteChanged to hide buttons
// otherwise link visibility to IsEnabled
@ -170,7 +192,8 @@ public class BetterContentDialog : ContentDialog
}
else
{
PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
PrimaryButton.IsVisible =
IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText);
}
}
@ -185,7 +208,8 @@ public class BetterContentDialog : ContentDialog
}
else
{
SecondaryButton.IsVisible = IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText);
SecondaryButton.IsVisible =
IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText);
}
}
@ -254,7 +278,8 @@ public class BetterContentDialog : ContentDialog
var border2 = faBorder?.Child as Border;
// 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;
@ -267,7 +292,8 @@ public class BetterContentDialog : ContentDialog
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 })

164
StabilityMatrix.Avalonia/DesignData/DesignData.cs

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

15
StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs

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

4
StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs

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

549
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -68,6 +68,42 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Close.
/// </summary>
public static string Action_Close {
get {
return ResourceManager.GetString("Action_Close", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Continue.
/// </summary>
public static string Action_Continue {
get {
return ResourceManager.GetString("Action_Continue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
public static string Action_Delete {
get {
return ResourceManager.GetString("Action_Delete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exit Application.
/// </summary>
public static string Action_ExitApplication {
get {
return ResourceManager.GetString("Action_ExitApplication", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import.
/// </summary>
@ -77,6 +113,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Install.
/// </summary>
public static string Action_Install {
get {
return ResourceManager.GetString("Action_Install", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch.
/// </summary>
@ -86,6 +131,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to New.
/// </summary>
public static string Action_New {
get {
return ResourceManager.GetString("Action_New", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open on CivitAI.
/// </summary>
public static string Action_OpenOnCivitAi {
get {
return ResourceManager.GetString("Action_OpenOnCivitAi", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Quit.
/// </summary>
@ -113,6 +176,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Rename.
/// </summary>
public static string Action_Rename {
get {
return ResourceManager.GetString("Action_Rename", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save.
/// </summary>
@ -122,6 +194,60 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Search.
/// </summary>
public static string Action_Search {
get {
return ResourceManager.GetString("Action_Search", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show in Explorer.
/// </summary>
public static string Action_ShowInExplorer {
get {
return ResourceManager.GetString("Action_ShowInExplorer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Advanced Options.
/// </summary>
public static string Label_AdvancedOptions {
get {
return ResourceManager.GetString("Label_AdvancedOptions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to All Versions.
/// </summary>
public static string Label_AllVersions {
get {
return ResourceManager.GetString("Label_AllVersions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Base Model.
/// </summary>
public static string Label_BaseModel {
get {
return ResourceManager.GetString("Label_BaseModel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Become a Patron.
/// </summary>
public static string Label_BecomeAPatron {
get {
return ResourceManager.GetString("Label_BecomeAPatron", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Branches.
/// </summary>
@ -131,6 +257,87 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Categories.
/// </summary>
public static string Label_Categories {
get {
return ResourceManager.GetString("Label_Categories", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Close dialog when finished.
/// </summary>
public static string Label_CloseDialogWhenFinished {
get {
return ResourceManager.GetString("Label_CloseDialogWhenFinished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Commit.
/// </summary>
public static string Label_Commit {
get {
return ResourceManager.GetString("Label_Commit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connected Model.
/// </summary>
public static string Label_ConnectedModel {
get {
return ResourceManager.GetString("Label_ConnectedModel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Data Directory.
/// </summary>
public static string Label_DataDirectory {
get {
return ResourceManager.GetString("Label_DataDirectory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This is where model checkpoints, LORAs, web UIs, settings, etc. will be installed..
/// </summary>
public static string Label_DataDirectoryExplanation {
get {
return ResourceManager.GetString("Label_DataDirectoryExplanation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Data provided by CivitAI.
/// </summary>
public static string Label_DataProvidedByCivitAi {
get {
return ResourceManager.GetString("Label_DataProvidedByCivitAi", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Display Name.
/// </summary>
public static string Label_DisplayName {
get {
return ResourceManager.GetString("Label_DisplayName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Downloads.
/// </summary>
public static string Label_Downloads {
get {
return ResourceManager.GetString("Label_Downloads", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Drag &amp; Drop checkpoints here to import.
/// </summary>
@ -140,6 +347,105 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Drop file here to import.
/// </summary>
public static string Label_DropFileToImport {
get {
return ResourceManager.GetString("Label_DropFileToImport", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience..
/// </summary>
public static string Label_FatWarning {
get {
return ResourceManager.GetString("Label_FatWarning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Find Connected Metadata.
/// </summary>
public static string Label_FindConnectedMetadata {
get {
return ResourceManager.GetString("Label_FindConnectedMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to First Page.
/// </summary>
public static string Label_FirstPage {
get {
return ResourceManager.GetString("Label_FirstPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Folder.
/// </summary>
public static string Label_Folder {
get {
return ResourceManager.GetString("Label_Folder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import as Connected.
/// </summary>
public static string Label_ImportAsConnected {
get {
return ResourceManager.GetString("Label_ImportAsConnected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search for connected metadata on new local imports.
/// </summary>
public static string Label_ImportAsConnectedExplanation {
get {
return ResourceManager.GetString("Label_ImportAsConnectedExplanation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import Latest -.
/// </summary>
public static string Label_ImportLatest {
get {
return ResourceManager.GetString("Label_ImportLatest", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Indexing....
/// </summary>
public static string Label_Indexing {
get {
return ResourceManager.GetString("Label_Indexing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An installation with this name already exists..
/// </summary>
public static string Label_InstallationWithThisNameExists {
get {
return ResourceManager.GetString("Label_InstallationWithThisNameExists", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Join Discord Server.
/// </summary>
public static string Label_JoinDiscord {
get {
return ResourceManager.GetString("Label_JoinDiscord", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Language.
/// </summary>
@ -149,6 +455,105 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Last Page.
/// </summary>
public static string Label_LastPage {
get {
return ResourceManager.GetString("Label_LastPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Let&apos;s get started.
/// </summary>
public static string Label_LetsGetStarted {
get {
return ResourceManager.GetString("Label_LetsGetStarted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to License Agreement..
/// </summary>
public static string Label_LicenseAgreement {
get {
return ResourceManager.GetString("Label_LicenseAgreement", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Local Model.
/// </summary>
public static string Label_LocalModel {
get {
return ResourceManager.GetString("Label_LocalModel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Model Description.
/// </summary>
public static string Label_ModelDescription {
get {
return ResourceManager.GetString("Label_ModelDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search models, #tags, or @users.
/// </summary>
public static string Label_ModelSearchWatermark {
get {
return ResourceManager.GetString("Label_ModelSearchWatermark", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Models Folder.
/// </summary>
public static string Label_ModelsFolder {
get {
return ResourceManager.GetString("Label_ModelsFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Model Type.
/// </summary>
public static string Label_ModelType {
get {
return ResourceManager.GetString("Label_ModelType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A new version of Stability Matrix is available!.
/// </summary>
public static string Label_NewVersionAvailable {
get {
return ResourceManager.GetString("Label_NewVersionAvailable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Next Image.
/// </summary>
public static string Label_NextImage {
get {
return ResourceManager.GetString("Label_NextImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Next Page.
/// </summary>
public static string Label_NextPage {
get {
return ResourceManager.GetString("Label_NextPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Package Type.
/// </summary>
@ -158,6 +563,78 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Page.
/// </summary>
public static string Label_Page {
get {
return ResourceManager.GetString("Label_Page", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please choose a different name or select a different install location..
/// </summary>
public static string Label_PleaseChooseDifferentName {
get {
return ResourceManager.GetString("Label_PleaseChooseDifferentName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Portable Mode.
/// </summary>
public static string Label_PortableMode {
get {
return ResourceManager.GetString("Label_PortableMode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to In Portable Mode, all data and settings will be stored in the same directory as the application. You will be able to move the application with its &apos;Data&apos; folder to a different location or computer..
/// </summary>
public static string Label_PortableModeExplanation {
get {
return ResourceManager.GetString("Label_PortableModeExplanation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Previous Image.
/// </summary>
public static string Label_PreviousImage {
get {
return ResourceManager.GetString("Label_PreviousImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Previous Page.
/// </summary>
public static string Label_PreviousPage {
get {
return ResourceManager.GetString("Label_PreviousPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PyTorch Version.
/// </summary>
public static string Label_PyTorchVersion {
get {
return ResourceManager.GetString("Label_PyTorchVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to I have read and agree to the.
/// </summary>
public static string Label_ReadAndAgree {
get {
return ResourceManager.GetString("Label_ReadAndAgree", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Relaunch Required.
/// </summary>
@ -176,6 +653,69 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Shared Model Folder Strategy.
/// </summary>
public static string Label_SharedModelFolderStrategy {
get {
return ResourceManager.GetString("Label_SharedModelFolderStrategy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show Model Images.
/// </summary>
public static string Label_ShowModelImages {
get {
return ResourceManager.GetString("Label_ShowModelImages", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show NSFW Content.
/// </summary>
public static string Label_ShowNsfwContent {
get {
return ResourceManager.GetString("Label_ShowNsfwContent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Skip first-time setup.
/// </summary>
public static string Label_SkipSetup {
get {
return ResourceManager.GetString("Label_SkipSetup", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Sort.
/// </summary>
public static string Label_Sort {
get {
return ResourceManager.GetString("Label_Sort", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Period.
/// </summary>
public static string Label_TimePeriod {
get {
return ResourceManager.GetString("Label_TimePeriod", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An unexpected error occurred.
/// </summary>
public static string Label_UnexpectedErrorOccurred {
get {
return ResourceManager.GetString("Label_UnexpectedErrorOccurred", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown Package.
/// </summary>
@ -185,6 +725,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Update Available.
/// </summary>
public static string Label_UpdateAvailable {
get {
return ResourceManager.GetString("Label_UpdateAvailable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version.
/// </summary>

33
StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx

@ -20,4 +20,37 @@
<data name="Label_Language" xml:space="preserve">
<value>言語</value>
</data>
<data name="Action_ExitApplication" xml:space="preserve">
<value>アプリケーションを終了する</value>
</data>
<data name="Action_Import" xml:space="preserve">
<value>インポート</value>
</data>
<data name="Action_Install" xml:space="preserve">
<value>インストール</value>
</data>
<data name="Action_Launch" xml:space="preserve">
<value>打ち上げる</value>
</data>
<data name="Action_Relaunch" xml:space="preserve">
<value>再起動する</value>
</data>
<data name="Action_Quit" xml:space="preserve">
<value>やめる</value>
</data>
<data name="Action_RelaunchLater" xml:space="preserve">
<value>後で再起動する</value>
</data>
<data name="Label_BecomeAPatron" xml:space="preserve">
<value>パトロンになる</value>
</data>
<data name="Label_Branches" xml:space="preserve">
<value>ブランチ</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>期間</value>
</data>
<data name="Label_Sort" xml:space="preserve">
<value>注文</value>
</data>
</root>

183
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -69,4 +69,187 @@
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve">
<value>Drag &amp; Drop checkpoints here to import</value>
</data>
<data name="Label_UpdateAvailable" xml:space="preserve">
<value>Update Available</value>
</data>
<data name="Label_BecomeAPatron" xml:space="preserve">
<value>Become a Patron</value>
</data>
<data name="Label_JoinDiscord" xml:space="preserve">
<value>Join Discord Server</value>
</data>
<data name="Label_Downloads" xml:space="preserve">
<value>Downloads</value>
</data>
<data name="Action_Install" xml:space="preserve">
<value>Install</value>
</data>
<data name="Label_SkipSetup" xml:space="preserve">
<value>Skip first-time setup</value>
</data>
<data name="Label_UnexpectedErrorOccurred" xml:space="preserve">
<value>An unexpected error occurred</value>
</data>
<data name="Action_ExitApplication" xml:space="preserve">
<value>Exit Application</value>
</data>
<data name="Label_DisplayName" xml:space="preserve">
<value>Display Name</value>
</data>
<data name="Label_InstallationWithThisNameExists" xml:space="preserve">
<value>An installation with this name already exists.</value>
</data>
<data name="Label_PleaseChooseDifferentName" xml:space="preserve">
<value>Please choose a different name or select a different install location.</value>
</data>
<data name="Label_AdvancedOptions" xml:space="preserve">
<value>Advanced Options</value>
</data>
<data name="Label_Commit" xml:space="preserve">
<value>Commit</value>
</data>
<data name="Label_SharedModelFolderStrategy" xml:space="preserve">
<value>Shared Model Folder Strategy</value>
</data>
<data name="Label_PyTorchVersion" xml:space="preserve">
<value>PyTorch Version</value>
</data>
<data name="Label_CloseDialogWhenFinished" xml:space="preserve">
<value>Close dialog when finished</value>
</data>
<data name="Action_Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="Label_DataDirectory" xml:space="preserve">
<value>Data Directory</value>
</data>
<data name="Label_DataDirectoryExplanation" xml:space="preserve">
<value>This is where model checkpoints, LORAs, web UIs, settings, etc. will be installed.</value>
</data>
<data name="Label_FatWarning" xml:space="preserve">
<value>You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience.</value>
</data>
<data name="Label_PortableMode" xml:space="preserve">
<value>Portable Mode</value>
</data>
<data name="Label_PortableModeExplanation" xml:space="preserve">
<value>In Portable Mode, all data and settings will be stored in the same directory as the application. You will be able to move the application with its 'Data' folder to a different location or computer.</value>
</data>
<data name="Action_Continue" xml:space="preserve">
<value>Continue</value>
</data>
<data name="Label_PreviousImage" xml:space="preserve">
<value>Previous Image</value>
</data>
<data name="Label_NextImage" xml:space="preserve">
<value>Next Image</value>
</data>
<data name="Label_ModelDescription" xml:space="preserve">
<value>Model Description</value>
</data>
<data name="Label_NewVersionAvailable" xml:space="preserve">
<value>A new version of Stability Matrix is available!</value>
</data>
<data name="Label_ImportLatest" xml:space="preserve">
<value>Import Latest -</value>
</data>
<data name="Label_AllVersions" xml:space="preserve">
<value>All Versions</value>
</data>
<data name="Label_ModelSearchWatermark" xml:space="preserve">
<value>Search models, #tags, or @users</value>
</data>
<data name="Action_Search" xml:space="preserve">
<value>Search</value>
</data>
<data name="Label_Sort" xml:space="preserve">
<value>Sort</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>Period</value>
</data>
<data name="Label_ModelType" xml:space="preserve">
<value>Model Type</value>
</data>
<data name="Label_BaseModel" xml:space="preserve">
<value>Base Model</value>
</data>
<data name="Label_ShowNsfwContent" xml:space="preserve">
<value>Show NSFW Content</value>
</data>
<data name="Label_DataProvidedByCivitAi" xml:space="preserve">
<value>Data provided by CivitAI</value>
</data>
<data name="Label_Page" xml:space="preserve">
<value>Page</value>
</data>
<data name="Label_FirstPage" xml:space="preserve">
<value>First Page</value>
</data>
<data name="Label_PreviousPage" xml:space="preserve">
<value>Previous Page</value>
</data>
<data name="Label_NextPage" xml:space="preserve">
<value>Next Page</value>
</data>
<data name="Label_LastPage" xml:space="preserve">
<value>Last Page</value>
</data>
<data name="Action_Rename" xml:space="preserve">
<value>Rename</value>
</data>
<data name="Action_Delete" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Action_OpenOnCivitAi" xml:space="preserve">
<value>Open on CivitAI</value>
</data>
<data name="Label_ConnectedModel" xml:space="preserve">
<value>Connected Model</value>
</data>
<data name="Label_LocalModel" xml:space="preserve">
<value>Local Model</value>
</data>
<data name="Action_ShowInExplorer" xml:space="preserve">
<value>Show in Explorer</value>
</data>
<data name="Action_New" xml:space="preserve">
<value>New</value>
</data>
<data name="Label_Folder" xml:space="preserve">
<value>Folder</value>
</data>
<data name="Label_DropFileToImport" xml:space="preserve">
<value>Drop file here to import</value>
</data>
<data name="Label_ImportAsConnected" xml:space="preserve">
<value>Import as Connected</value>
</data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Search for connected metadata on new local imports</value>
</data>
<data name="Label_Indexing" xml:space="preserve">
<value>Indexing...</value>
</data>
<data name="Label_ModelsFolder" xml:space="preserve">
<value>Models Folder</value>
</data>
<data name="Label_Categories" xml:space="preserve">
<value>Categories</value>
</data>
<data name="Label_LetsGetStarted" xml:space="preserve">
<value>Let's get started</value>
</data>
<data name="Label_ReadAndAgree" xml:space="preserve">
<value>I have read and agree to the</value>
</data>
<data name="Label_LicenseAgreement" xml:space="preserve">
<value>License Agreement.</value>
</data>
<data name="Label_FindConnectedMetadata" xml:space="preserve">
<value>Find Connected Metadata</value>
</data>
<data name="Label_ShowModelImages" xml:space="preserve">
<value>Show Model Images</value>
</data>
</root>

60
StabilityMatrix.Avalonia/Program.cs

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

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

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

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

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

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

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

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

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

288
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -51,40 +51,84 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 14;
private LRUCache<int /* model id */, CheckpointBrowserCardViewModel> cache = new(50);
[ObservableProperty] private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards;
[ObservableProperty] private DataGridCollectionView? modelCardsView;
[ObservableProperty] private string searchQuery = string.Empty;
[ObservableProperty] private bool showNsfw;
[ObservableProperty] private bool showMainLoadingSpinner;
[ObservableProperty] private CivitPeriod selectedPeriod = CivitPeriod.Month;
[ObservableProperty] private CivitSortMode sortMode = CivitSortMode.HighestRated;
[ObservableProperty] private CivitModelType selectedModelType = CivitModelType.Checkpoint;
[ObservableProperty] private int currentPageNumber;
[ObservableProperty] private int totalPages;
[ObservableProperty] private bool hasSearched;
[ObservableProperty] private bool canGoToNextPage;
[ObservableProperty] private bool canGoToPreviousPage;
[ObservableProperty] private bool canGoToFirstPage;
[ObservableProperty] private bool canGoToLastPage;
[ObservableProperty] private bool isIndeterminate;
[ObservableProperty] private bool noResultsFound;
[ObservableProperty] private string noResultsText = string.Empty;
[ObservableProperty] private string selectedBaseModelType = "All";
private LRUCache<
int /* model id */
,
CheckpointBrowserCardViewModel
> cache = new(50);
[ObservableProperty]
private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards;
[ObservableProperty]
private DataGridCollectionView? modelCardsView;
[ObservableProperty]
private string searchQuery = string.Empty;
[ObservableProperty]
private bool showNsfw;
[ObservableProperty]
private bool showMainLoadingSpinner;
[ObservableProperty]
private CivitPeriod selectedPeriod = CivitPeriod.Month;
[ObservableProperty]
private CivitSortMode sortMode = CivitSortMode.HighestRated;
[ObservableProperty]
private CivitModelType selectedModelType = CivitModelType.Checkpoint;
[ObservableProperty]
private int currentPageNumber;
[ObservableProperty]
private int totalPages;
[ObservableProperty]
private bool hasSearched;
[ObservableProperty]
private bool canGoToNextPage;
[ObservableProperty]
private bool canGoToPreviousPage;
[ObservableProperty]
private bool canGoToFirstPage;
[ObservableProperty]
private bool canGoToLastPage;
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
private bool noResultsFound;
[ObservableProperty]
private string noResultsText = string.Empty;
[ObservableProperty]
private string selectedBaseModelType = "All";
private List<CheckpointBrowserCardViewModel> allModelCards = new();
public IEnumerable<CivitPeriod> AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>();
public IEnumerable<CivitSortMode> AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast<CivitSortMode>();
public IEnumerable<CivitPeriod> AllCivitPeriods =>
Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>();
public IEnumerable<CivitSortMode> AllSortModes =>
Enum.GetValues(typeof(CivitSortMode)).Cast<CivitSortMode>();
public IEnumerable<CivitModelType> AllModelTypes => Enum.GetValues(typeof(CivitModelType))
public IEnumerable<CivitModelType> AllModelTypes =>
Enum.GetValues(typeof(CivitModelType))
.Cast<CivitModelType>()
.Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0)
.OrderBy(t => t.ToString());
public List<string> BaseModelOptions => new() {"All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0"};
public List<string> BaseModelOptions =>
new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" };
public CheckpointBrowserViewModel(
ICivitApi civitApi,
@ -92,7 +136,8 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
ILiteDbContext liteDbContext,
INotificationService notificationService)
INotificationService notificationService
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
@ -117,15 +162,23 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
public override void OnLoaded()
{
if (Design.IsDesignMode) return;
if (Design.IsDesignMode)
return;
var searchOptions = settingsManager.Settings.ModelSearchOptions;
// Fix SelectedModelType if someone had selected the obsolete "Model" option
if (searchOptions is { SelectedModelType: CivitModelType.Model })
{
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod, SortMode, CivitModelType.Checkpoint, SelectedBaseModelType));
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
CivitModelType.Checkpoint,
SelectedBaseModelType
)
);
searchOptions = settingsManager.Settings.ModelSearchOptions;
}
@ -136,8 +189,11 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
ShowNsfw = settingsManager.Settings.ModelBrowserNsfwEnabled;
settingsManager.RelayPropertyFor(this, model => model.ShowNsfw,
settings => settings.ModelBrowserNsfwEnabled);
settingsManager.RelayPropertyFor(
this,
model => model.ShowNsfw,
settings => settings.ModelBrowserNsfwEnabled
);
}
/// <summary>
@ -145,7 +201,8 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
/// </summary>
private bool FilterModelCardsPredicate(object? item)
{
if (item is not CheckpointBrowserCardViewModel card) return false;
if (item is not CheckpointBrowserCardViewModel card)
return false;
return !card.CivitModel.Nsfw || ShowNsfw;
}
@ -162,38 +219,52 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
var models = modelsResponse.Items;
if (models is null)
{
Logger.Debug("CivitAI Query {Text} returned no results (in {Elapsed:F1} s)",
queryText, timer.Elapsed.TotalSeconds);
Logger.Debug(
"CivitAI Query {Text} returned no results (in {Elapsed:F1} s)",
queryText,
timer.Elapsed.TotalSeconds
);
return;
}
Logger.Debug("CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)",
queryText, models.Count, timer.Elapsed.TotalSeconds);
Logger.Debug(
"CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)",
queryText,
models.Count,
timer.Elapsed.TotalSeconds
);
var unknown = models.Where(m => m.Type == CivitModelType.Unknown).ToList();
if (unknown.Any())
{
var names = unknown.Select(m => m.Name).ToList();
Logger.Warn("Excluded {Unknown} unknown model types: {Models}", unknown.Count,
names);
Logger.Warn(
"Excluded {Unknown} unknown model types: {Models}",
unknown.Count,
names
);
}
// Filter out unknown model types and archived/taken-down models
models = models.Where(m => m.Type.ConvertTo<SharedFolderType>() > 0)
.Where(m => m.Mode == null).ToList();
models = models
.Where(m => m.Type.ConvertTo<SharedFolderType>() > 0)
.Where(m => m.Mode == null)
.ToList();
// Database update calls will invoke `OnModelsUpdated`
// Add to database
await liteDbContext.UpsertCivitModelAsync(models);
// Add as cache entry
var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync(new()
var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync(
new()
{
Id = ObjectHash.GetMd5Guid(request),
InsertedAt = DateTimeOffset.UtcNow,
Request = request,
Items = models,
Metadata = modelsResponse.Metadata
});
}
);
if (cacheNew)
{
@ -207,26 +278,42 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
}
catch (OperationCanceledException)
{
notificationService.Show(new Notification("Request to CivitAI timed out",
"Please try again in a few minutes"));
notificationService.Show(
new Notification(
"Request to CivitAI timed out",
"Please try again in a few minutes"
)
);
Logger.Warn($"CivitAI query timed out ({request})");
}
catch (HttpRequestException e)
{
notificationService.Show(new Notification("CivitAI can't be reached right now",
"Please try again in a few minutes"));
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
"Please try again in a few minutes"
)
);
Logger.Warn(e, $"CivitAI query HttpRequestException ({request})");
}
catch (ApiException e)
{
notificationService.Show(new Notification("CivitAI can't be reached right now",
"Please try again in a few minutes"));
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
"Please try again in a few minutes"
)
);
Logger.Warn(e, $"CivitAI query ApiException ({request})");
}
catch (Exception e)
{
notificationService.Show(new Notification("CivitAI can't be reached right now",
$"Unknown exception during CivitAI query: {e.GetType().Name}"));
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
$"Unknown exception during CivitAI query: {e.GetType().Name}"
)
);
Logger.Error(e, $"CivitAI query unknown exception ({request})");
}
finally
@ -266,7 +353,8 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{
if (cache.Get(viewModel.CivitModel.Id) != null) return;
if (cache.Get(viewModel.CivitModel.Id) != null)
return;
cache.Add(viewModel.CivitModel.Id, viewModel);
};
@ -274,15 +362,17 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
});
return newCard;
}).ToList();
})
.ToList();
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed)
{
filteredCards =
filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available");
filteredCards = filteredCards.OrderByDescending(
x => x.UpdateCardText == "Update Available"
);
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
@ -347,8 +437,8 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
if (SortMode == CivitSortMode.Installed)
{
var connectedModels =
CheckpointFile.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
if (SelectedModelType != CivitModelType.All)
@ -358,10 +448,16 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
modelRequest = new CivitModelsRequest
{
CommaSeparatedModelIds = string.Join(",",
connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m)
.Select(g => g.First())),
Types = SelectedModelType == CivitModelType.All ? null : new[] {SelectedModelType}
CommaSeparatedModelIds = string.Join(
",",
connectedModels
.Select(c => c.ConnectedModel!.ModelId)
.GroupBy(m => m)
.Select(g => g.First())
),
Types =
SelectedModelType == CivitModelType.All ? null : new[] { SelectedModelType },
Page = CurrentPageNumber,
};
}
@ -374,8 +470,12 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
if (cachedQuery is not null)
{
var elapsed = timer.Elapsed;
Logger.Debug("Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)",
SearchQuery, modelRequest.GetHashCode(), elapsed.TotalSeconds);
Logger.Debug(
"Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)",
SearchQuery,
modelRequest.GetHashCode(),
elapsed.TotalSeconds
);
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata);
// Start remote query (background mode)
@ -386,7 +486,8 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
CivitModelQuery(modelRequest).SafeFireAndForget();
Logger.Debug(
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query",
timeSinceCache.Value.TotalSeconds);
timeSinceCache.Value.TotalSeconds
);
}
}
else
@ -429,11 +530,11 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
{
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value);
// ModelCardsView?.Refresh();
var updateCards = allModelCards
.Where(FilterModelCardsPredicate);
var updateCards = allModelCards.Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);
if (!HasSearched) return;
if (!HasSearched)
return;
UpdateResultsText();
}
@ -441,34 +542,63 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
partial void OnSelectedPeriodChanged(CivitPeriod value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
value, SortMode, SelectedModelType, SelectedBaseModelType));
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
value,
SortMode,
SelectedModelType,
SelectedBaseModelType
)
);
}
partial void OnSortModeChanged(CivitSortMode value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod, value, SelectedModelType, SelectedBaseModelType));
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
value,
SelectedModelType,
SelectedBaseModelType
)
);
}
partial void OnSelectedModelTypeChanged(CivitModelType value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod, SortMode, value, SelectedBaseModelType));
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
value,
SelectedBaseModelType
)
);
}
partial void OnSelectedBaseModelTypeChanged(string value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod, SortMode, SelectedModelType, value));
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
SelectedModelType,
value
)
);
}
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)
{
if (!HasSearched) return;
if (!HasSearched)
return;
ModelCards?.Clear();
if (shouldUpdatePageNumber)
@ -483,11 +613,13 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
private void UpdateResultsText()
{
NoResultsFound = ModelCards?.Count <= 0;
NoResultsText = allModelCards.Count > 0
NoResultsText =
allModelCards.Count > 0
? $"{allModelCards.Count} results hidden by filters"
: "No results found";
}
public override string Title => "Model Browser";
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };
}

138
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs

@ -45,14 +45,24 @@ public partial class CheckpointFile : ViewModelBase
private ConnectedModelInfo? connectedModel;
public bool IsConnectedModel => ConnectedModel != null;
[ObservableProperty] private bool isLoading;
[ObservableProperty] private CivitModelType modelType;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private CivitModelType modelType;
public string FileName => Path.GetFileName(FilePath);
public ObservableCollection<string> Badges { get; set; } = new();
private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" };
public static readonly string[] SupportedCheckpointExtensions =
{
".safetensors",
".pt",
".ckpt",
".pth",
".bin"
};
private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" };
private static readonly string[] SupportedMetadataExtensions = { ".json" };
@ -78,7 +88,8 @@ public partial class CheckpointFile : ViewModelBase
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException(
"Cannot get connected model info file path when FilePath is empty");
"Cannot get connected model info file path when FilePath is empty"
);
}
var modelNameNoExt = Path.GetFileNameWithoutExtension((string?)FilePath);
var modelDir = Path.GetDirectoryName((string?)FilePath) ?? "";
@ -134,11 +145,11 @@ public partial class CheckpointFile : ViewModelBase
Label = "File name",
Validator = text =>
{
if (string.IsNullOrWhiteSpace(text)) throw new
DataValidationException("File name is required");
if (string.IsNullOrWhiteSpace(text))
throw new DataValidationException("File name is required");
if (File.Exists(Path.Combine(parentPath, text))) throw new
DataValidationException("File name already exists");
if (File.Exists(Path.Combine(parentPath, text)))
throw new DataValidationException("File name already exists");
},
Text = FileName
}
@ -160,7 +171,10 @@ public partial class CheckpointFile : ViewModelBase
// If preview image exists, rename it too
if (PreviewImagePath != null && File.Exists(PreviewImagePath))
{
var newPreviewImagePath = Path.Combine(parentPath, $"{nameNoExt}.preview{Path.GetExtension((string?) PreviewImagePath)}");
var newPreviewImagePath = Path.Combine(
parentPath,
$"{nameNoExt}.preview{Path.GetExtension((string?)PreviewImagePath)}"
);
File.Move(PreviewImagePath, newPreviewImagePath);
PreviewImagePath = newPreviewImagePath;
}
@ -170,7 +184,10 @@ public partial class CheckpointFile : ViewModelBase
var cmInfoPath = Path.Combine(parentPath, $"{originalNameNoExt}.cm-info.json");
if (File.Exists(cmInfoPath))
{
File.Move(cmInfoPath, Path.Combine(parentPath, $"{nameNoExt}.cm-info.json"));
File.Move(
cmInfoPath,
Path.Combine(parentPath, $"{nameNoExt}.cm-info.json")
);
}
}
}
@ -184,7 +201,8 @@ public partial class CheckpointFile : ViewModelBase
[RelayCommand]
private void OpenOnCivitAi()
{
if (ConnectedModel?.ModelId == null) return;
if (ConnectedModel?.ModelId == null)
return;
ProcessRunner.OpenUrl($"https://civitai.com/models/{ConnectedModel.ModelId}");
}
@ -195,12 +213,20 @@ public partial class CheckpointFile : ViewModelBase
/// - {filename}.preview.{image-extensions} (preview image)
/// - {filename}.cm-info.json (connected model info)
/// </summary>
public static IEnumerable<CheckpointFile> FromDirectoryIndex(string directory, SearchOption searchOption = SearchOption.TopDirectoryOnly)
public static IEnumerable<CheckpointFile> FromDirectoryIndex(
string directory,
SearchOption searchOption = SearchOption.TopDirectoryOnly
)
{
foreach (var file in Directory.EnumerateFiles(directory, "*.*", searchOption))
{
if (!SupportedCheckpointExtensions.Any(ext =>
Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase)))
if (
!SupportedCheckpointExtensions.Any(
ext =>
Path.GetExtension(file)
.Equals(ext, StringComparison.InvariantCultureIgnoreCase)
)
)
continue;
var checkpointFile = new CheckpointFile
@ -209,7 +235,10 @@ public partial class CheckpointFile : ViewModelBase
FilePath = Path.Combine(directory, file),
};
var jsonPath = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.cm-info.json");
var jsonPath = Path.Combine(
directory,
$"{Path.GetFileNameWithoutExtension(file)}.cm-info.json"
);
if (File.Exists(jsonPath))
{
var json = File.ReadAllText(jsonPath);
@ -218,8 +247,14 @@ public partial class CheckpointFile : ViewModelBase
}
checkpointFile.PreviewImagePath = SupportedImageExtensions
.Select(ext => Path.Combine(directory,
$"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists)
.Select(
ext =>
Path.Combine(
directory,
$"{Path.GetFileNameWithoutExtension(file)}.preview{ext}"
)
)
.Where(File.Exists)
.FirstOrDefault();
yield return checkpointFile;
@ -228,10 +263,21 @@ public partial class CheckpointFile : ViewModelBase
public static IEnumerable<CheckpointFile> GetAllCheckpointFiles(string modelsDirectory)
{
foreach (var file in Directory.EnumerateFiles(modelsDirectory, "*.*", SearchOption.AllDirectories))
{
if (!SupportedCheckpointExtensions.Any(ext =>
Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase)))
foreach (
var file in Directory.EnumerateFiles(
modelsDirectory,
"*.*",
SearchOption.AllDirectories
)
)
{
if (
!SupportedCheckpointExtensions.Any(
ext =>
Path.GetExtension(file)
.Equals(ext, StringComparison.InvariantCultureIgnoreCase)
)
)
continue;
var checkpointFile = new CheckpointFile
@ -240,8 +286,10 @@ public partial class CheckpointFile : ViewModelBase
FilePath = file,
};
var jsonPath = Path.Combine(Path.GetDirectoryName(file) ?? "",
Path.GetFileNameWithoutExtension(file) + ".cm-info.json");
var jsonPath = Path.Combine(
Path.GetDirectoryName(file) ?? "",
Path.GetFileNameWithoutExtension(file) + ".cm-info.json"
);
if (File.Exists(jsonPath))
{
@ -252,8 +300,14 @@ public partial class CheckpointFile : ViewModelBase
}
checkpointFile.PreviewImagePath = SupportedImageExtensions
.Select(ext => Path.Combine(Path.GetDirectoryName(file) ?? "",
$"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists)
.Select(
ext =>
Path.Combine(
Path.GetDirectoryName(file) ?? "",
$"{Path.GetFileNameWithoutExtension(file)}.preview{ext}"
)
)
.Where(File.Exists)
.FirstOrDefault();
yield return checkpointFile;
@ -263,8 +317,11 @@ public partial class CheckpointFile : ViewModelBase
/// <summary>
/// Index with progress reporting.
/// </summary>
public static IEnumerable<CheckpointFile> FromDirectoryIndex(string directory, IProgress<ProgressReport> progress,
SearchOption searchOption = SearchOption.TopDirectoryOnly)
public static IEnumerable<CheckpointFile> FromDirectoryIndex(
string directory,
IProgress<ProgressReport> progress,
SearchOption searchOption = SearchOption.TopDirectoryOnly
)
{
var current = 0ul;
foreach (var checkpointFile in FromDirectoryIndex(directory, searchOption))
@ -309,4 +366,31 @@ public partial class CheckpointFile : ViewModelBase
return CivitModelType.Unknown;
}
private sealed class FilePathEqualityComparer : IEqualityComparer<CheckpointFile>
{
public bool Equals(CheckpointFile? x, CheckpointFile? y)
{
if (ReferenceEquals(x, y))
return true;
if (ReferenceEquals(x, null))
return false;
if (ReferenceEquals(y, null))
return false;
if (x.GetType() != y.GetType())
return false;
return x.FilePath == y.FilePath
&& x.ConnectedModel?.Hashes.BLAKE3 == y.ConnectedModel?.Hashes.BLAKE3
&& x.ConnectedModel?.ThumbnailImageUrl == y.ConnectedModel?.ThumbnailImageUrl
&& x.PreviewImagePath == y.PreviewImagePath;
}
public int GetHashCode(CheckpointFile obj)
{
return obj.FilePath.GetHashCode();
}
}
public static IEqualityComparer<CheckpointFile> FilePathComparer { get; } =
new FilePathEqualityComparer();
}

249
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs

@ -5,13 +5,16 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
@ -28,6 +31,14 @@ public partial class CheckpointFolder : ViewModelBase
private readonly ISettingsManager settingsManager;
private readonly IDownloadService downloadService;
private readonly ModelFinder modelFinder;
private readonly INotificationService notificationService;
public SourceCache<CheckpointFolder, string> SubFoldersCache { get; } =
new(x => x.DirectoryPath);
private readonly SourceCache<CheckpointFile, string> checkpointFilesCache =
new(x => x.FilePath);
// ReSharper disable once FieldCanBeMadeReadOnly.Local
private bool useCategoryVisibility;
@ -50,12 +61,14 @@ public partial class CheckpointFolder : ViewModelBase
/// <summary>
/// True if the category is enabled for the manager page.
/// </summary>
[ObservableProperty] private bool isCategoryEnabled = true;
[ObservableProperty]
private bool isCategoryEnabled = true;
/// <summary>
/// True if currently expanded in the UI.
/// </summary>
[ObservableProperty] private bool isExpanded = true;
[ObservableProperty]
private bool isExpanded = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))]
@ -65,7 +78,8 @@ public partial class CheckpointFolder : ViewModelBase
[NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))]
private bool isImportInProgress;
[ObservableProperty] private string searchFilter = string.Empty;
[ObservableProperty]
private string searchFilter = string.Empty;
public bool IsDragBlurEnabled => IsCurrentDragTarget || IsImportInProgress;
@ -77,23 +91,58 @@ public partial class CheckpointFolder : ViewModelBase
public ProgressViewModel Progress { get; } = new();
public CheckpointFolder? ParentFolder { get; init; }
public AdvancedObservableList<CheckpointFolder> SubFolders { get; init; } = new();
public AdvancedObservableList<CheckpointFile> CheckpointFiles { get; init; } = new();
public AdvancedObservableList<CheckpointFile> DisplayedCheckpointFiles { get; set; }
public IObservableCollection<CheckpointFolder> SubFolders { get; } =
new ObservableCollectionExtended<CheckpointFolder>();
public IObservableCollection<CheckpointFile> CheckpointFiles { get; } =
new ObservableCollectionExtended<CheckpointFile>();
public IObservableCollection<CheckpointFile> DisplayedCheckpointFiles { get; } =
new ObservableCollectionExtended<CheckpointFile>();
public CheckpointFolder(
ISettingsManager settingsManager,
IDownloadService downloadService,
ModelFinder modelFinder,
bool useCategoryVisibility = true)
INotificationService notificationService,
bool useCategoryVisibility = true
)
{
this.settingsManager = settingsManager;
this.downloadService = downloadService;
this.modelFinder = modelFinder;
this.notificationService = notificationService;
this.useCategoryVisibility = useCategoryVisibility;
checkpointFilesCache
.Connect()
.DeferUntilLoaded()
.Bind(CheckpointFiles)
.Sort(
SortExpressionComparer<CheckpointFile>
.Descending(f => f.IsConnectedModel)
.ThenByAscending(
f => f.IsConnectedModel ? f.ConnectedModel!.ModelName : f.FileName
)
)
.Filter(
f =>
f.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)
|| f.Title.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)
)
.Bind(DisplayedCheckpointFiles)
.Subscribe();
SubFoldersCache
.Connect()
.DeferUntilLoaded()
.SortBy(x => x.Title)
.Bind(SubFolders)
.Subscribe();
CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged;
DisplayedCheckpointFiles = CheckpointFiles;
// DisplayedCheckpointFiles = CheckpointFiles;
}
/// <summary>
@ -102,7 +151,8 @@ public partial class CheckpointFolder : ViewModelBase
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnTitleChanged(string value)
{
if (!useCategoryVisibility) return;
if (!useCategoryVisibility)
return;
// Update folder type
var result = Enum.TryParse(Title, out SharedFolderType type);
@ -113,16 +163,12 @@ public partial class CheckpointFolder : ViewModelBase
partial void OnSearchFilterChanged(string value)
{
if (string.IsNullOrEmpty(value))
foreach (var subFolder in SubFolders)
{
DisplayedCheckpointFiles = CheckpointFiles;
}
else
{
var filteredFiles = CheckpointFiles.Where(y =>
y.FileName.Contains(value, StringComparison.OrdinalIgnoreCase));
DisplayedCheckpointFiles = new AdvancedObservableList<CheckpointFile>(filteredFiles);
subFolder.SearchFilter = value;
}
checkpointFilesCache.Refresh();
}
/// <summary>
@ -130,7 +176,8 @@ public partial class CheckpointFolder : ViewModelBase
/// </summary>
partial void OnIsCategoryEnabledChanged(bool value)
{
if (!useCategoryVisibility) return;
if (!useCategoryVisibility)
return;
if (value != settingsManager.IsSharedFolderCategoryVisible(FolderType))
{
settingsManager.SetSharedFolderCategoryVisible(FolderType, value);
@ -181,7 +228,9 @@ public partial class CheckpointFolder : ViewModelBase
}
var dialog = DialogHelper.CreateTaskDialog(
"Are you sure you want to delete this folder?",directory);
"Are you sure you want to delete this folder?",
directory
);
dialog.ShowProgressBar = false;
dialog.Buttons = new List<TaskDialogButton>
@ -235,51 +284,70 @@ public partial class CheckpointFolder : ViewModelBase
if (result == ContentDialogResult.Primary)
{
var targetName = textBox.Text;
if (string.IsNullOrWhiteSpace(targetName)) return;
if (string.IsNullOrWhiteSpace(targetName))
return;
var subFolderPath = Path.Combine(DirectoryPath, targetName);
Directory.CreateDirectory(subFolderPath);
SubFolders.Add(new CheckpointFolder(settingsManager,
downloadService, modelFinder,
useCategoryVisibility: false)
SubFolders.Add(
new CheckpointFolder(
settingsManager,
downloadService,
modelFinder,
notificationService,
useCategoryVisibility: false
)
{
Title = Path.GetFileName(subFolderPath),
DirectoryPath = subFolderPath,
FolderType = FolderType,
ParentFolder = this,
IsExpanded = false,
});
}
);
}
}
/// <summary>
/// Imports files to the folder. Reports progress to instance properties.
/// </summary>
public async Task ImportFilesAsync(IEnumerable<string> files, bool convertToConnected = false)
public async Task ImportFilesAsync(
IEnumerable<string> files,
bool convertToConnected = false,
bool copyFiles = true
)
{
try
{
Progress.Value = 0;
var copyPaths = files.ToDictionary(k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v)));
var copyPaths = files.ToDictionary(
k => k,
v => Path.Combine(DirectoryPath, Path.GetFileName(v))
);
var progress = new Progress<ProgressReport>(report =>
{
Progress.IsIndeterminate = false;
Progress.Value = report.Percentage;
// For multiple files, add count
Progress.Text = copyPaths.Count > 1 ? $"Importing {report.Title} ({report.Message})" : $"Importing {report.Title}";
Progress.Text =
copyPaths.Count > 1
? $"Importing {report.Title} ({report.Message})"
: $"Importing {report.Title}";
});
if (copyFiles)
{
await FileTransfers.CopyFiles(copyPaths, progress);
}
// Hash files and convert them to connected model if found
if (convertToConnected)
{
var modelFilesCount = copyPaths.Count;
var modelFiles = copyPaths.Values
.Select(path => new FilePath(path));
var modelFiles = copyPaths.Values.Select(path => new FilePath(path));
// Holds tasks for model queries after hash
var modelQueryTasks = new List<Task<bool>>();
@ -290,9 +358,10 @@ public partial class CheckpointFolder : ViewModelBase
{
Progress.IsIndeterminate = report.IsIndeterminate;
Progress.Value = report.Percentage;
Progress.Text = modelFilesCount > 1 ?
$"Computing metadata for {modelFile.Name} ({i}/{modelFilesCount})" :
$"Computing metadata for {modelFile.Name}";
Progress.Text =
modelFilesCount > 1
? $"Computing metadata for {modelFile.Name} ({i}/{modelFilesCount})"
: $"Computing metadata for {modelFile.Name}";
});
var hashBlake3 = await FileHash.GetBlake3Async(modelFile, hashProgress);
@ -303,14 +372,19 @@ public partial class CheckpointFolder : ViewModelBase
var result = await modelFinder.LocalFindModel(hashBlake3);
result ??= await modelFinder.RemoteFindModel(hashBlake3);
if (result is null) return false; // Not found
if (result is null)
return false; // Not found
var (model, version, file) = result.Value;
// Save connected model info json
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name);
var modelInfo = new ConnectedModelInfo(
model, version, file, DateTimeOffset.UtcNow);
model,
version,
file,
DateTimeOffset.UtcNow
);
await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName);
// If available, save thumbnail
@ -321,8 +395,15 @@ public partial class CheckpointFolder : ViewModelBase
if (imageExt is "jpg" or "jpeg" or "png")
{
var imageDownloadPath = Path.GetFullPath(
Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}"));
await downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
Path.Combine(
DirectoryPath,
$"{modelFileName}.preview.{imageExt}"
)
);
await downloadService.DownloadToFileAsync(
image.Url,
imageDownloadPath
);
}
}
@ -342,17 +423,15 @@ public partial class CheckpointFolder : ViewModelBase
var totalCount = modelQueryResults.Length;
var failCount = totalCount - successCount;
await IndexAsync();
BackgroundIndex();
Progress.Value = 100;
Progress.Text = successCount switch
{
0 when failCount > 0 =>
"Import complete. No connected data found.",
> 0 when failCount > 0 =>
$"Import complete. Found connected data for {successCount} of {totalCount} models.",
1 when failCount == 0 =>
"Import complete. Found connected data for 1 model.",
0 when failCount > 0 => "Import complete. No connected data found.",
> 0 when failCount > 0
=> $"Import complete. Found connected data for {successCount} of {totalCount} models.",
1 when failCount == 0 => "Import complete. Found connected data for 1 model.",
_ => $"Import complete. Found connected data for all {totalCount} models."
};
}
@ -360,7 +439,7 @@ public partial class CheckpointFolder : ViewModelBase
{
Progress.Text = "Import complete";
Progress.Value = 100;
await IndexAsync();
BackgroundIndex();
}
}
finally
@ -369,12 +448,35 @@ public partial class CheckpointFolder : ViewModelBase
}
}
public async Task FindConnectedMetadata()
{
IsImportInProgress = true;
var files = CheckpointFiles
.Where(f => !f.IsConnectedModel)
.Select(f => f.FilePath)
.ToList();
if (files.Any())
{
await ImportFilesAsync(files, true, false);
}
else
{
notificationService.Show(
"Cannot Find Connected Metadata",
"All files in this folder are already connected.",
NotificationType.Warning
);
}
}
/// <summary>
/// Clears progress after a delay.
/// </summary>
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay).ContinueWith(_ =>
Task.Delay(delay)
.ContinueWith(_ =>
{
IsImportInProgress = false;
Progress.Value = 0;
@ -386,35 +488,47 @@ public partial class CheckpointFolder : ViewModelBase
/// <summary>
/// Gets checkpoint files from folder index
/// </summary>
private async Task<List<CheckpointFile>> GetCheckpointFilesAsync(IProgress<ProgressReport>? progress = default)
private IEnumerable<CheckpointFile> GetCheckpointFiles()
{
if (!Directory.Exists(DirectoryPath))
{
return new List<CheckpointFile>();
return Enumerable.Empty<CheckpointFile>();
}
return await (progress switch
{
null => Task.Run(() =>
CheckpointFile.FromDirectoryIndex(DirectoryPath).ToList()),
return CheckpointFile.FromDirectoryIndex(DirectoryPath);
}
_ => Task.Run(() =>
CheckpointFile.FromDirectoryIndex(DirectoryPath, progress).ToList())
});
/// <summary>
/// Indexes the folder for checkpoint files and refreshes the CheckPointFiles collection.
/// </summary>
public void BackgroundIndex()
{
Dispatcher.UIThread.Post(Index, DispatcherPriority.Background);
}
/// <summary>
/// Indexes the folder for checkpoint files and refreshes the CheckPointFiles collection.
/// </summary>
public async Task IndexAsync(IProgress<ProgressReport>? progress = default)
public void Index()
{
SubFolders.Clear();
// Get subfolders
foreach (var folder in Directory.GetDirectories(DirectoryPath))
{
// Get from cache or create new
if (SubFoldersCache.Lookup(folder) is { HasValue: true } result)
{
result.Value.BackgroundIndex();
}
else
{
// Create subfolder
var subFolder = new CheckpointFolder(settingsManager,
downloadService, modelFinder, useCategoryVisibility: false)
var subFolder = new CheckpointFolder(
settingsManager,
downloadService,
modelFinder,
notificationService,
useCategoryVisibility: false
)
{
Title = Path.GetFileName(folder),
DirectoryPath = folder,
@ -422,12 +536,19 @@ public partial class CheckpointFolder : ViewModelBase
ParentFolder = this,
IsExpanded = false, // Subfolders are collapsed by default
};
await subFolder.IndexAsync(progress);
SubFolders.Add(subFolder);
subFolder.BackgroundIndex();
SubFoldersCache.AddOrUpdate(subFolder);
}
}
CheckpointFiles.Clear();
CheckpointFiles.AddRange(await GetCheckpointFilesAsync());
// Index files
Dispatcher.UIThread.Post(
() =>
{
var files = GetCheckpointFiles();
checkpointFilesCache.EditDiff(files, CheckpointFile.FilePathComparer);
},
DispatcherPriority.Background
);
}
}

172
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -1,15 +1,16 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.Views;
@ -30,64 +31,90 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
private readonly ISettingsManager settingsManager;
private readonly ModelFinder modelFinder;
private readonly IDownloadService downloadService;
private readonly INotificationService notificationService;
public override string Title => "Checkpoints";
public override IconSource IconSource => new SymbolIconSource
{Symbol = Symbol.Notebook, IsFilled = true};
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Notebook, IsFilled = true };
// Toggle button for auto hashing new drag-and-dropped files for connected upgrade
[ObservableProperty] private bool isImportAsConnected;
[ObservableProperty] private bool isLoading;
[ObservableProperty] private bool isIndexing;
[ObservableProperty]
private bool isImportAsConnected;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private bool isIndexing;
[ObservableProperty]
private bool showConnectedModelImages;
[ObservableProperty]
private string searchFilter = string.Empty;
partial void OnIsImportAsConnectedChanged(bool value)
{
if (settingsManager.IsLibraryDirSet &&
value != settingsManager.Settings.IsImportAsConnected)
if (
settingsManager.IsLibraryDirSet && value != settingsManager.Settings.IsImportAsConnected
)
{
settingsManager.Transaction(s => s.IsImportAsConnected = value);
}
}
[ObservableProperty]
private ObservableCollection<CheckpointManager.CheckpointFolder> checkpointFolders = new();
public SourceCache<CheckpointFolder, string> CheckpointFoldersCache { get; } =
new(x => x.DirectoryPath);
[ObservableProperty]
private ObservableCollection<CheckpointManager.CheckpointFolder> displayedCheckpointFolders = new();
public IObservableCollection<CheckpointFolder> CheckpointFolders { get; } =
new ObservableCollectionExtended<CheckpointFolder>();
public IObservableCollection<CheckpointFolder> DisplayedCheckpointFolders { get; } =
new ObservableCollectionExtended<CheckpointFolder>();
public CheckpointsPageViewModel(
ISharedFolders sharedFolders,
ISettingsManager settingsManager,
IDownloadService downloadService,
ModelFinder modelFinder)
INotificationService notificationService,
ModelFinder modelFinder
)
{
this.sharedFolders = sharedFolders;
this.settingsManager = settingsManager;
this.downloadService = downloadService;
this.notificationService = notificationService;
this.modelFinder = modelFinder;
CheckpointFoldersCache
.Connect()
.DeferUntilLoaded()
.SortBy(x => x.Title)
.Bind(CheckpointFolders)
.Filter(ContainsSearchFilter)
.Bind(DisplayedCheckpointFolders)
.Subscribe();
}
public override async Task OnLoadedAsync()
{
var sw = Stopwatch.StartNew();
DisplayedCheckpointFolders = CheckpointFolders;
// DisplayedCheckpointFolders = CheckpointFolders;
// Set UI states
IsImportAsConnected = settingsManager.Settings.IsImportAsConnected;
ShowConnectedModelImages = settingsManager.Settings.ShowConnectedModelImages;
// Refresh search filter
OnSearchFilterChanged(string.Empty);
Logger.Info($"Loaded {DisplayedCheckpointFolders.Count} checkpoint folders in {sw.ElapsedMilliseconds}ms");
if (Design.IsDesignMode) return;
if (Design.IsDesignMode)
return;
IsLoading = CheckpointFolders.Count == 0;
IsIndexing = CheckpointFolders.Count > 0;
await IndexFolders();
// GetStuff();
IndexFolders();
IsLoading = false;
IsIndexing = false;
@ -97,55 +124,47 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnSearchFilterChanged(string value)
{
var sw = Stopwatch.StartNew();
if (string.IsNullOrWhiteSpace(SearchFilter))
{
DisplayedCheckpointFolders = new ObservableCollection<CheckpointFolder>(
CheckpointFolders.Select(x =>
foreach (var folder in CheckpointFolders)
{
x.SearchFilter = SearchFilter;
return x;
}));
sw.Stop();
Logger.Info($"OnSearchFilterChanged in {sw.ElapsedMilliseconds}ms");
return;
folder.SearchFilter = value;
}
sw.Restart();
CheckpointFoldersCache.Refresh();
}
var filteredFolders = CheckpointFolders
.Where(ContainsSearchFilter).ToList();
foreach (var folder in filteredFolders)
partial void OnShowConnectedModelImagesChanged(bool value)
{
folder.SearchFilter = SearchFilter;
if (
settingsManager.IsLibraryDirSet
&& value != settingsManager.Settings.ShowConnectedModelImages
)
{
settingsManager.Transaction(s => s.ShowConnectedModelImages = value);
}
sw.Stop();
Logger.Info($"ContainsSearchFilter in {sw.ElapsedMilliseconds}ms");
DisplayedCheckpointFolders = new ObservableCollection<CheckpointFolder>(filteredFolders);
}
private bool ContainsSearchFilter(CheckpointManager.CheckpointFolder folder)
private bool ContainsSearchFilter(CheckpointFolder folder)
{
if (folder == null)
throw new ArgumentNullException(nameof(folder));
if (string.IsNullOrWhiteSpace(SearchFilter))
{
return true;
}
// Check files in the current folder
return folder.CheckpointFiles.Any(x =>
x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)) ||
return folder.CheckpointFiles.Any(
x => x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)
)
||
// If no matching files were found in the current folder, check in all subfolders
folder.SubFolders.Any(subFolder => ContainsSearchFilter(subFolder));
folder.SubFolders.Any(ContainsSearchFilter);
}
private async Task IndexFolders()
private void IndexFolders()
{
var modelsDirectory = settingsManager.ModelsDirectory;
// Get all folders within the shared folder root
if (string.IsNullOrWhiteSpace(modelsDirectory))
{
CheckpointFolders.Clear();
return;
}
// Setup shared folders in case they're missing
sharedFolders.SetupSharedModelFolders();
@ -155,45 +174,34 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
var sw = Stopwatch.StartNew();
// Index all folders
var indexTasks = folders.Select(async f =>
{
var checkpointFolder =
new CheckpointFolder(settingsManager, downloadService, modelFinder)
{
Title = Path.GetFileName(f),
DirectoryPath = f,
IsExpanded = true, // Top level folders expanded by default
};
await checkpointFolder.IndexAsync();
return checkpointFolder;
}).ToList();
await Task.WhenAll(indexTasks);
sw.Stop();
Logger.Info($"IndexFolders in {sw.ElapsedMilliseconds}ms");
// Set new observable collection, ordered by alphabetical order
CheckpointFolders =
new ObservableCollection<CheckpointFolder>(indexTasks
.Select(t => t.Result)
.OrderBy(f => f.Title));
if (!string.IsNullOrWhiteSpace(SearchFilter))
foreach (var folder in folders)
{
var filtered = CheckpointFolders
.Where(x => x.CheckpointFiles.Any(y => y.FileName.Contains(SearchFilter))).Select(
f =>
// Get from cache or create new
if (CheckpointFoldersCache.Lookup(folder) is { HasValue: true } result)
{
f.SearchFilter = SearchFilter;
return f;
});
DisplayedCheckpointFolders = new ObservableCollection<CheckpointFolder>(filtered);
result.Value.Index();
}
else
{
DisplayedCheckpointFolders = CheckpointFolders;
var checkpointFolder = new CheckpointFolder(
settingsManager,
downloadService,
modelFinder,
notificationService
)
{
Title = Path.GetFileName(folder),
DirectoryPath = folder,
IsExpanded = true // Top level folders expanded by default
};
checkpointFolder.Index();
CheckpointFoldersCache.AddOrUpdate(checkpointFolder);
}
}
sw.Stop();
Logger.Info($"IndexFolders in {sw.Elapsed.TotalMilliseconds:F1}ms");
}
[RelayCommand]

109
StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs

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

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

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

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

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

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

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

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

@ -27,13 +27,26 @@ public partial class OneClickInstallViewModel : ViewModelBase
private readonly ISharedFolders sharedFolders;
private 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];
}
@ -107,16 +125,31 @@ public partial class OneClickInstallViewModel : ViewModelBase
// get latest version & download & install
SubHeaderText = "Getting latest version...";
var latestVersion = await SelectedPackage.GetLatestVersion();
SelectedPackage.InstallLocation =
Path.Combine(libraryDir, "Packages", SelectedPackage.Name);
SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text;
var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name);
var downloadVersion = new DownloadPackageVersionOptions();
var installedVersion = new InstalledPackageVersion();
var versionOptions = await SelectedPackage.GetAllVersionOptions();
if (versionOptions.AvailableVersions != null && versionOptions.AvailableVersions.Any())
{
downloadVersion.VersionTag = versionOptions.AvailableVersions.First().TagName;
installedVersion.InstalledReleaseVersion = downloadVersion.VersionTag;
}
else
{
downloadVersion.BranchName = await SelectedPackage.GetLatestVersion();
installedVersion.InstalledBranch = downloadVersion.BranchName;
}
var torchVersion = SelectedPackage.GetRecommendedTorchVersion();
await DownloadPackage(latestVersion);
await InstallPackage();
await DownloadPackage(installLocation, downloadVersion);
await InstallPackage(installLocation, torchVersion);
SubHeaderText = "Setting up shared folder links...";
await SelectedPackage.SetupModelFolders(SelectedPackage.InstallLocation);
var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
await SelectedPackage.SetupModelFolders(installLocation, recommendedSharedFolderMethod);
var installedPackage = new InstalledPackage
{
@ -124,10 +157,11 @@ public partial class OneClickInstallViewModel : ViewModelBase
LibraryPath = Path.Combine("Packages", SelectedPackage.Name),
Id = Guid.NewGuid(),
PackageName = SelectedPackage.Name,
PackageVersion = latestVersion,
DisplayVersion = latestVersion,
Version = installedVersion,
LaunchCommand = SelectedPackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now
LastUpdateCheck = DateTimeOffset.Now,
PreferredTorchVersion = torchVersion,
PreferredSharedFolderMethod = recommendedSharedFolderMethod
};
await using var st = settingsManager.BeginTransaction();
st.Settings.InstalledPackages.Add(installedPackage);
@ -148,7 +182,10 @@ public partial class OneClickInstallViewModel : ViewModelBase
EventManager.Instance.OnOneClickInstallFinished(false);
}
private async Task DownloadPackage(string version)
private async Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions
)
{
SubHeaderText = "Downloading package...";
@ -159,14 +196,13 @@ public partial class OneClickInstallViewModel : ViewModelBase
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
});
await SelectedPackage.DownloadPackage(version, false, version, progress);
await SelectedPackage.DownloadPackage(installLocation, versionOptions, progress);
SubHeaderText = "Download Complete";
OneClickInstallProgress = 100;
}
private async Task InstallPackage()
private async Task InstallPackage(string installLocation, TorchVersion torchVersion)
{
SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text;
SubHeaderText = "Downloading and installing package requirements...";
var progress = new Progress<ProgressReport>(progress =>
@ -177,6 +213,11 @@ public partial class OneClickInstallViewModel : ViewModelBase
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
});
await SelectedPackage.InstallPackage(progress);
await SelectedPackage.InstallPackage(
installLocation,
torchVersion,
progress,
(output) => SubSubHeaderText = output.Text
);
}
}

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

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

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

@ -9,6 +9,7 @@ using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
@ -30,18 +31,28 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
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";
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 dataDirectory = DefaultInstallLocation;
[ObservableProperty] private string directoryStatusText = string.Empty;
[ObservableProperty] private bool isStatusBadgeVisible;
[ObservableProperty] private bool isDirectoryValid;
[ObservableProperty] private bool showFatWarning;
[ObservableProperty]
private bool isPortableMode;
public RefreshBadgeViewModel ValidatorRefreshBadge { get; } = new()
[ObservableProperty]
private string directoryStatusText = string.Empty;
[ObservableProperty]
private bool isStatusBadgeVisible;
[ObservableProperty]
private bool isDirectoryValid;
[ObservableProperty]
private bool showFatWarning;
public RefreshBadgeViewModel ValidatorRefreshBadge { get; } =
new()
{
State = ProgressState.Inactive,
SuccessToolTipText = ValidExistingDirectoryText,
@ -89,10 +100,10 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
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;
@ -142,13 +153,12 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
private async Task ShowFolderBrowserDialog()
{
var provider = App.StorageProvider;
var result = await provider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Data Folder",
AllowMultiple = false
});
var result = await provider.OpenFolderPickerAsync(
new FolderPickerOpenOptions { Title = "Select Data Folder", AllowMultiple = false }
);
if (result.Count != 1) return;
if (result.Count != 1)
return;
DataDirectory = result[0].Path.LocalPath;
}

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

@ -9,6 +9,7 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -23,13 +24,26 @@ 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))]
@ -37,8 +51,10 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public int DisplayedPageNumber => SelectedImageIndex + 1;
public SelectModelVersionViewModel(ISettingsManager settingsManager,
IDownloadService downloadService)
public SelectModelVersionViewModel(
ISettingsManager settingsManager,
IDownloadService downloadService
)
{
this.settingsManager = settingsManager;
this.downloadService = downloadService;
@ -53,8 +69,10 @@ 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())
{
@ -92,14 +110,16 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public void PreviousImage()
{
if (SelectedImageIndex > 0) SelectedImageIndex--;
if (SelectedImageIndex > 0)
SelectedImageIndex--;
CanGoToPreviousImage = SelectedImageIndex > 0;
CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1;
}
public void NextImage()
{
if (SelectedImageIndex < ImageUrls.Count - 1) SelectedImageIndex++;
if (SelectedImageIndex < ImageUrls.Count - 1)
SelectedImageIndex++;
CanGoToPreviousImage = SelectedImageIndex > 0;
CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1;
}

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

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

172
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -56,22 +56,34 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
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]
private bool launchButtonVisibility;
[ObservableProperty, NotifyPropertyChangedFor(nameof(SelectedBasePackage),
nameof(SelectedPackageExtraCommands))]
[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 ObservableCollection<InstalledPackage> installedPackages = new();
[ObservableProperty] private BasePackage? runningPackage;
[ObservableProperty]
private BasePackage? runningPackage;
public virtual BasePackage? SelectedBasePackage =>
PackageFactory.FindPackageByName(SelectedPackage?.PackageName);
@ -83,8 +95,11 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
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,
@ -93,7 +108,8 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
IPyRunner pyRunner,
INotificationService notificationService,
ISharedFolders sharedFolders,
ServiceManager<ViewModelBase> dialogFactory)
ServiceManager<ViewModelBase> dialogFactory
)
{
this.logger = logger;
this.settingsManager = settingsManager;
@ -103,9 +119,11 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
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;
@ -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,14 +163,18 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
public override void OnLoaded()
{
// Ensure active package either exists or is null
settingsManager.Transaction(s =>
settingsManager.Transaction(
s =>
{
s.UpdateActiveInstalledPackage();
}, ignoreMissingLibraryDir: true);
},
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;
@ -170,10 +194,13 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
if (activeInstall == null)
{
// No selected package: error notification
notificationService.Show(new Notification(
notificationService.Show(
new Notification(
message: "You must install and select a package before launching",
title: "No package selected",
type: NotificationType.Error));
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);
activeInstallName
);
notificationService.Show(new Notification("Package name invalid",
notificationService.Show(
new Notification(
"Package name invalid",
"Install package name did not match a definition. Please reinstall and let us know about this issue.",
NotificationType.Error));
NotificationType.Error
)
);
return;
}
@ -205,12 +237,12 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
.FromDefinitions(definitions, Array.Empty<LaunchOption>())
.ToImmutableArray();
var args = cards
.SelectMany(c => c.Options)
.ToList();
var args = cards.SelectMany(c => c.Options).ToList();
logger.LogDebug("Setting initial launch args: {Args}",
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr())));
logger.LogDebug(
"Setting initial launch args: {Args}",
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr()))
);
settingsManager.SaveLaunchArgs(activeInstall.Id, args);
}
@ -223,7 +255,6 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Unpack sitecustomize.py to venv
await UnpackSiteCustomize(packagePath.JoinDir("venv"));
basePackage.ConsoleOutput += OnProcessOutputReceived;
basePackage.Exited += OnProcessExited;
basePackage.StartupComplete += RunningPackageOnStartupComplete;
@ -233,7 +264,10 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
Console.StartUpdates();
// Update shared folder links (in case library paths changed)
await basePackage.UpdateModelFolders(packagePath);
await basePackage.UpdateModelFolders(
packagePath,
activeInstall.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod
);
// Load user launch args from settings and convert to string
var userArgs = settingsManager.GetLaunchArgs(activeInstall.Id);
@ -245,10 +279,12 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Use input command if provided, otherwise use package launch command
command ??= basePackage.LaunchCommand;
await basePackage.RunPackage(packagePath, command, userArgsString);
await basePackage.RunPackage(packagePath, command, userArgsString, OnProcessOutputReceived);
RunningPackage = basePackage;
EventManager.Instance.OnRunningPackageStatusChanged(new PackagePair(activeInstall, basePackage));
EventManager.Instance.OnRunningPackageStatusChanged(
new PackagePair(activeInstall, basePackage)
);
}
// Unpacks sitecustomize.py to the target venv
@ -292,11 +328,11 @@ 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
{
@ -308,10 +344,7 @@ 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();
@ -372,7 +405,8 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
public virtual async Task Stop()
{
if (RunningPackage is null) return;
if (RunningPackage is null)
return;
await RunningPackage.WaitForShutdown();
RunningPackage = null;
@ -383,19 +417,27 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
public void OpenWebUi()
{
if (string.IsNullOrEmpty(webUiUrl)) return;
if (string.IsNullOrEmpty(webUiUrl))
return;
notificationService.TryAsync(Task.Run(() => ProcessRunner.OpenUrl(webUiUrl)),
"Failed to open URL", $"{webUiUrl}");
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 () =>
Dispatcher.UIThread
.InvokeAsync(async () =>
{
logger.LogTrace("Process exited ({Code}) at {Time:g}",
exitCode, DateTimeOffset.Now);
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)
@ -411,7 +453,10 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
}
catch (OperationCanceledException e)
{
logger.LogWarning("Waiting for process EOF timed out: {Message}", e.Message);
logger.LogWarning(
"Waiting for process EOF timed out: {Message}",
e.Message
);
}
}
}
@ -419,7 +464,6 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Detach handlers
if (sender is BasePackage basePackage)
{
basePackage.ConsoleOutput -= OnProcessOutputReceived;
basePackage.Exited -= OnProcessExited;
basePackage.StartupComplete -= RunningPackageOnStartupComplete;
}
@ -431,13 +475,15 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Need to reset cursor in case its in some weird position
// from progress bars
await Console.ResetWriteCursor();
Console.PostLine($"{Environment.NewLine}Process finished with exit code {exitCode}");
}).SafeFireAndForget();
Console.PostLine(
$"{Environment.NewLine}Process finished with exit code {exitCode}"
);
})
.SafeFireAndForget();
}
// Callback for processes
private void OnProcessOutputReceived(object? sender, ProcessOutput output)
private void OnProcessOutputReceived(ProcessOutput output)
{
Console.Post(output);
EventManager.Instance.OnScrollToBottomRequested();
@ -464,23 +510,29 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
e.Cancel = true;
var dialog = CreateExitConfirmDialog();
Dispatcher.UIThread.InvokeAsync(async () =>
Dispatcher.UIThread
.InvokeAsync(async () =>
{
if ((TaskDialogStandardResult)
await dialog.ShowAsync(true) == TaskDialogStandardResult.Yes)
if (
(TaskDialogStandardResult)await dialog.ShowAsync(true)
== TaskDialogStandardResult.Yes
)
{
App.Services.GetRequiredService<MainWindow>().Hide();
App.Shutdown();
}
}).SafeFireAndForget();
})
.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;

28
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

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

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

@ -34,11 +34,20 @@ 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,
@ -46,7 +55,8 @@ public partial class PackageCardViewModel : ProgressViewModel
INotificationService notificationService,
ISettingsManager settingsManager,
INavigationService navigationService,
ServiceManager<ViewModelBase> vmFactory)
ServiceManager<ViewModelBase> vmFactory
)
{
this.logger = logger;
this.packageFactory = packageFactory;
@ -72,9 +82,8 @@ public partial class PackageCardViewModel : ProgressViewModel
IsUnknownPackage = false;
var basePackage = packageFactory[value.PackageName];
CardImageSource = basePackage?.PreviewImageUri.ToString()
?? Assets.NoImage.ToString();
InstalledVersion = value.DisplayVersion ?? "Unknown";
CardImageSource = basePackage?.PreviewImageUri.ToString() ?? Assets.NoImage.ToString();
InstalledVersion = value.Version?.DisplayVersion ?? "Unknown";
}
}
@ -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
@ -120,13 +130,19 @@ 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",
notificationService.Show(
new Notification(
"Success",
$"Package {Package.DisplayName} uninstalled",
NotificationType.Success));
NotificationType.Success
)
);
if (!IsUnknownPackage)
{
@ -143,16 +159,21 @@ public partial class PackageCardViewModel : ProgressViewModel
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;
}
@ -162,14 +183,16 @@ public partial class PackageCardViewModel : ProgressViewModel
IsIndeterminate = true;
var progressId = Guid.NewGuid();
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
EventManager.Instance.OnProgressChanged(
new ProgressItem(
progressId,
Package.DisplayName ?? Package.PackageName!,
new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update)));
new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update)
)
);
try
{
basePackage.InstallLocation = Package.FullPath!;
var progress = new Progress<ProgressReport>(progress =>
{
var percent = Convert.ToInt32(progress.Percentage);
@ -179,35 +202,54 @@ public partial class PackageCardViewModel : ProgressViewModel
Text = $"Updating {Package.DisplayName}";
EventManager.Instance.OnGlobalProgressChanged(percent);
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
packageName, progress));
EventManager.Instance.OnProgressChanged(
new ProgressItem(progressId, packageName, progress)
);
});
var updateResult = await basePackage.Update(Package, progress);
var torchVersion =
Package.PreferredTorchVersion ?? basePackage.GetRecommendedTorchVersion();
var updateResult = await basePackage.Update(Package, torchVersion, progress);
settingsManager.UpdatePackageVersionNumber(Package.Id, updateResult);
notificationService.Show("Update complete",
notificationService.Show(
"Update complete",
$"{Package.DisplayName} has been updated to the latest version.",
NotificationType.Success);
NotificationType.Success
);
await using (settingsManager.BeginTransaction())
{
Package.UpdateAvailable = false;
}
IsUpdateAvailable = false;
InstalledVersion = Package.DisplayVersion ?? "Unknown";
InstalledVersion = updateResult.DisplayVersion ?? "Unknown";
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
EventManager.Instance.OnProgressChanged(
new ProgressItem(
progressId,
packageName,
new ProgressReport(1f, "Update complete", type: ProgressType.Update)));
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,
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));
new ProgressReport(0f, "Update failed", type: ProgressType.Update),
Failed: true
)
);
}
finally
{
@ -219,27 +261,23 @@ public partial class PackageCardViewModel : ProgressViewModel
public async Task Import()
{
if (!IsUnknownPackage || Design.IsDesignMode) return;
if (!IsUnknownPackage || Design.IsDesignMode)
return;
var viewModel = vmFactory.Get<PackageImportViewModel>(vm =>
{
vm.PackagePath =
new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException());
vm.PackagePath = new DirectoryPath(
Package?.FullPath ?? throw new InvalidOperationException()
);
});
var dialog = new TaskDialog
{
Content = new PackageImportDialog
{
DataContext = viewModel
},
Content = new PackageImportDialog { DataContext = viewModel },
ShowProgressBar = false,
Buttons = new List<TaskDialogButton>
{
new(Resources.Action_Import, TaskDialogStandardResult.Yes)
{
IsDefault = true
},
new(Resources.Action_Import, TaskDialogStandardResult.Yes) { IsDefault = true },
new(Resources.Action_Cancel, TaskDialogStandardResult.Cancel)
}
};
@ -256,7 +294,9 @@ public partial class PackageCardViewModel : ProgressViewModel
await using (new MinimumDelay(200, 300))
{
var result = await notificationService.TryAsync(viewModel.AddPackageWithCurrentInputs());
var result = await notificationService.TryAsync(
viewModel.AddPackageWithCurrentInputs()
);
if (result.IsSuccessful)
{
EventManager.Instance.OnInstalledPackagesChanged();
@ -289,8 +329,9 @@ public partial class PackageCardViewModel : ProgressViewModel
if (basePackage == null)
return false;
var canCheckUpdate = Package.LastUpdateCheck == null ||
Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15);
var canCheckUpdate =
Package.LastUpdateCheck == null
|| Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15);
if (!canCheckUpdate)
{

49
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

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

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

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

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

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

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

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

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

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

218
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -55,58 +55,59 @@ public partial class SettingsViewModel : PageViewModelBase
public SharedState SharedState { get; }
public 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;
[ObservableProperty]
private string? selectedTheme;
public IReadOnlyList<string> AvailableThemes { get; } = new[]
{
"Light",
"Dark",
"System",
};
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,
};
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;
[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,
@ -116,7 +117,8 @@ public partial class SettingsViewModel : PageViewModelBase
ServiceManager<ViewModelBase> dialogFactory,
SharedState sharedState,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService)
IModelIndexService modelIndexService
)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
@ -133,23 +135,26 @@ public partial class SettingsViewModel : PageViewModelBase
RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown;
SelectedAnimationScale = settingsManager.Settings.AnimationScale;
settingsManager.RelayPropertyFor(this,
vm => vm.SelectedTheme,
settings => settings.Theme);
settingsManager.RelayPropertyFor(this, vm => vm.SelectedTheme, settings => settings.Theme);
settingsManager.RelayPropertyFor(this,
settingsManager.RelayPropertyFor(
this,
vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled);
settings => settings.IsDiscordRichPresenceEnabled
);
settingsManager.RelayPropertyFor(this,
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,12 +166,12 @@ 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);
@ -191,8 +196,11 @@ 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
);
}
}
@ -205,8 +213,11 @@ 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
@ -217,17 +228,15 @@ public partial class SettingsViewModel : PageViewModelBase
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",
@ -288,8 +297,7 @@ 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;
}
@ -297,18 +305,21 @@ public partial class SettingsViewModel : PageViewModelBase
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");
WindowsShortcuts.CreateShortcut(shortcutLink, appPath, iconPath, "Stability Matrix");
notificationService.Show("Added to Start Menu",
"Stability Matrix has been added to the Start Menu.", NotificationType.Success);
notificationService.Show(
"Added to Start Menu",
"Stability Matrix has been added to the Start Menu.",
NotificationType.Success
);
}
/// <summary>
@ -320,15 +331,15 @@ 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",
@ -344,7 +355,8 @@ public partial class SettingsViewModel : PageViewModelBase
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs");
"Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
@ -355,19 +367,26 @@ public partial class SettingsViewModel : PageViewModelBase
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)
@ -378,8 +397,11 @@ public partial class SettingsViewModel : PageViewModelBase
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();
@ -475,10 +494,13 @@ public partial class SettingsViewModel : PageViewModelBase
[RelayCommand]
private void DebugNotification()
{
notificationService.Show(new Notification(
notificationService.Show(
new Notification(
title: "Test Notification",
message: "Here is some message",
type: NotificationType.Information));
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]
@ -515,14 +536,8 @@ public partial class SettingsViewModel : PageViewModelBase
{
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,7 +557,8 @@ public partial class SettingsViewModel : PageViewModelBase
public void OnVersionClick()
{
// Ignore if already enabled
if (SharedState.IsDebugMode) return;
if (SharedState.IsDebugMode)
return;
VersionTapCount++;
@ -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
}

31
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -9,6 +9,7 @@
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700"
x:DataType="viewModels:CheckpointBrowserViewModel"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
@ -149,7 +150,7 @@
Margin="0,8,0,0">
<Button.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Import Latest -" />
<TextBlock Text="{x:Static lang:Resources.Label_ImportLatest}" />
<TextBlock Margin="4,0,0,0"
Text="{Binding CivitModel.FullFilesSize}" />
</StackPanel>
@ -162,7 +163,7 @@
IsEnabled="{Binding !IsImporting}"
Command="{Binding ShowVersionDialogCommand}"
CommandParameter="{Binding CivitModel}"
Content="All Versions" />
Content="{x:Static lang:Resources.Label_AllVersions}" />
</Grid>
</StackPanel>
</controls:Card>
@ -177,7 +178,7 @@
<TextBox
HorizontalAlignment="Stretch"
Margin="8,0,0,0"
Watermark="Search models, #tags, or @users"
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}"
Text="{Binding SearchQuery, Mode=TwoWay}" />
<Button
@ -197,7 +198,7 @@
IsVisible="{Binding SearchModelsCommand.IsRunning}"
MinWidth="16" />
<TextBlock
Text="Search"
Text="{x:Static lang:Resources.Action_Search}"
VerticalAlignment="Center"
IsVisible="{Binding !SearchModelsCommand.IsRunning}" />
</Grid>
@ -205,7 +206,7 @@
</Grid>
<DockPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Sort" />
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
ItemsSource="{Binding AllSortModes}"
MinWidth="100"
@ -213,7 +214,7 @@
</StackPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Period" />
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" />
<ComboBox
ItemsSource="{Binding AllCivitPeriods}"
MinWidth="100"
@ -221,7 +222,7 @@
</StackPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Model Type" />
<Label Content="{x:Static lang:Resources.Label_ModelType}" />
<ComboBox
ItemsSource="{Binding AllModelTypes}"
MinWidth="100"
@ -229,7 +230,7 @@
</StackPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Base Model" />
<Label Content="{x:Static lang:Resources.Label_BaseModel}" />
<ComboBox
ItemsSource="{Binding BaseModelOptions}"
MinWidth="100"
@ -238,7 +239,7 @@
<CheckBox
Content="Show NSFW Content"
Content="{x:Static lang:Resources.Label_ShowNsfwContent}"
HorizontalAlignment="Right"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}"
Margin="8,8,8,0" />
@ -258,7 +259,7 @@
<TextBlock
Grid.Row="2"
Margin="16,8"
Text="Data provided by CivitAI"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}"
VerticalAlignment="Bottom" />
<StackPanel
@ -268,7 +269,7 @@
Orientation="Vertical"
IsVisible="{Binding HasSearched}">
<TextBlock Margin="0,0,4,4" TextAlignment="Center">
<Run Text="Page" />
<Run Text="{x:Static lang:Resources.Label_Page}" />
<Run Text="{Binding CurrentPageNumber, FallbackValue=1}" />
<Run Text="/" />
<Run Text="{Binding TotalPages, FallbackValue=5}" />
@ -277,26 +278,26 @@
<Button
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="First Page"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}"
Margin="0,0,8,0">
<avalonia:Icon Value="fa-solid fa-backward-fast"/>
</Button>
<Button
Command="{Binding PreviousPage}"
ToolTip.Tip="Previous Page"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
Margin="0,0,8,0">
<avalonia:Icon Value="fa-solid fa-caret-left"/>
</Button>
<Button Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="Next Page"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}"
Margin="0,0,8,0">
<avalonia:Icon Value="fa-solid fa-caret-right"/>
</Button>
<Button
Command="{Binding LastPage}"
ToolTip.Tip="Last Page"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}"
IsEnabled="{Binding CanGoToLastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast"/>
</Button>

85
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -54,23 +54,23 @@
<controls:Card.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem Command="{Binding RenameCommand}"
Text="Rename" IconSource="Rename" />
Text="{x:Static lang:Resources.Action_Rename}" IconSource="Rename" />
<ui:MenuFlyoutItem Command="{Binding DeleteCommand}"
Text="Delete" IconSource="Delete" />
Text="{x:Static lang:Resources.Action_Delete}" IconSource="Delete" />
<ui:MenuFlyoutItem Command="{Binding OpenOnCivitAiCommand}"
Text="Open on CivitAI" IconSource="Link"
Text="{x:Static lang:Resources.Action_OpenOnCivitAi}" IconSource="Link"
IsVisible="{Binding IsConnectedModel}"/>
</ui:FAMenuFlyout>
</controls:Card.ContextFlyout>
<Grid>
<!-- Main contents, hidden when IsLoading is true -->
<StackPanel MinHeight="70">
<Grid ColumnDefinitions="*,*,*,*,*" RowDefinitions="*,0.1*" IsVisible="{Binding !IsLoading}">
<Grid ColumnDefinitions="*,*,*,*,*" RowDefinitions="*,Auto,0.1*" IsVisible="{Binding !IsLoading}">
<StackPanel
Grid.ColumnSpan="4"
HorizontalAlignment="Left"
IsEnabled="True"
Margin="4"
Margin="0,0,0,4"
Orientation="Vertical">
<!-- Tooltip for title and version -->
<ToolTip.Tip>
@ -119,6 +119,16 @@
TextWrapping="WrapWithOverflow"
IsVisible="{Binding IsConnectedModel}" />
</StackPanel>
<controls:BetterAdvancedImage Grid.Row="1"
Grid.Column="0" Grid.ColumnSpan="5"
Margin="0,0,0,4"
CornerRadius="4"
Source="{Binding PreviewImagePath}"
Stretch="UniformToFill"
MaxHeight="250"
IsVisible="{Binding $parent[ItemsControl].((vm:CheckpointsPageViewModel)DataContext).ShowConnectedModelImages}" />
<Grid Grid.Column="4" Grid.Row="0">
<Button
Background="Transparent"
@ -135,7 +145,7 @@
<ui:SymbolIcon
Foreground="LightGreen"
Symbol="CloudSyncComplete"
ToolTip.Tip="Connected Model" />
ToolTip.Tip="{x:Static lang:Resources.Label_ConnectedModel}" />
</Button>
<Button
Background="Transparent"
@ -152,7 +162,7 @@
<ui:SymbolIcon
Foreground="DimGray"
Symbol="CloudOffline"
ToolTip.Tip="Local Model" />
ToolTip.Tip="{x:Static lang:Resources.Label_LocalModel}" />
</Button>
</Grid>
<TextBlock
@ -160,29 +170,25 @@
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
Grid.Row="2"
IsEnabled="True"
Margin="4,4"
Text="{Binding FileName}"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding FileName}"
VerticalAlignment="Bottom" />
<ItemsControl
<ItemsRepeater
Grid.Column="3"
Grid.ColumnSpan="2"
Grid.Row="1"
Grid.Row="2"
ItemTemplate="{StaticResource BadgeTemplate}"
ItemsSource="{Binding Badges}"
VerticalAlignment="Bottom">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
HorizontalAlignment="Right"
Orientation="Horizontal"
VerticalAlignment="Bottom" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<ItemsRepeater.Layout>
<UniformGridLayout/>
</ItemsRepeater.Layout>
</ItemsRepeater>
</Grid>
</StackPanel>
@ -215,21 +221,24 @@
<!-- Right click menu for a checkpoint folder -->
<Expander.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem Text="Show in Explorer" IconSource="Open"
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Action_ShowInExplorer}" IconSource="Open"
Command="{Binding ShowInExplorerCommand}"
CommandParameter="{Binding DirectoryPath}"/>
<!-- Only allow deletion of non-root folders (Parent is not null) -->
<ui:MenuFlyoutItem Text="Delete" IconSource="Delete"
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Action_Delete}" IconSource="Delete"
IsEnabled="{Binding ParentFolder, Converter={x:Static ObjectConverters.IsNotNull}}"
IsVisible="{Binding ParentFolder, Converter={x:Static ObjectConverters.IsNotNull}}"
Command="{Binding DeleteCommand}"/>
<ui:MenuFlyoutSeparator/>
<ui:MenuFlyoutSubItem Text="New" IconSource="Add">
<ui:MenuFlyoutSubItem Text="{x:Static lang:Resources.Action_New}" IconSource="Add">
<ui:MenuFlyoutSubItem.Items>
<ui:MenuFlyoutItem Text="Folder" IconSource="OpenFolder"
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Label_Folder}" IconSource="OpenFolder"
Command="{Binding CreateSubFolderCommand}"/>
</ui:MenuFlyoutSubItem.Items>
</ui:MenuFlyoutSubItem>
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Label_FindConnectedMetadata}"
IconSource="Find"
Command="{Binding FindConnectedMetadata}"/>
</ui:FAMenuFlyout>
</Expander.ContextFlyout>
@ -284,7 +293,7 @@
Effect="{StaticResource TextDropShadowEffect}"
FontSize="24"
HorizontalAlignment="Center"
Text="Drop file here to import"
Text="{x:Static lang:Resources.Label_DropFileToImport}"
VerticalAlignment="Center"
IsVisible="{Binding IsCurrentDragTarget}" />
<StackPanel
@ -355,15 +364,23 @@
</controls:UserControlBase.Resources>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="0.2*,*" Margin="4, 0">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*" Margin="4, 0">
<!-- Top settings bar -->
<StackPanel Grid.Column="0" Grid.Row="0">
<ToggleSwitch
OffContent="Import as Connected"
OnContent="Import as Connected"
Grid.Column="0"
OffContent="{x:Static lang:Resources.Label_ImportAsConnected}"
OnContent="{x:Static lang:Resources.Label_ImportAsConnected}"
IsChecked="{Binding IsImportAsConnected}"
Margin="24,8"
ToolTip.Tip="Search for connected metadata on new local imports" />
Margin="24,4,24,-4"
ToolTip.Tip="{x:Static lang:Resources.Label_ImportAsConnectedExplanation}" />
<ToggleSwitch
OffContent="{x:Static lang:Resources.Label_ShowModelImages}"
OnContent="{x:Static lang:Resources.Label_ShowModelImages}"
IsChecked="{Binding ShowConnectedModelImages}"
Margin="24,-4,24,4" />
</StackPanel>
<StackPanel
Grid.Column="1"
Grid.Row="0"
@ -372,7 +389,7 @@
Margin="16,0"
Orientation="Horizontal">
<TextBox Margin="16, 16"
Watermark="Search"
Watermark="{x:Static lang:Resources.Action_Search}"
Height="16"
MinWidth="150"
Text="{Binding SearchFilter, Mode=TwoWay}"/>
@ -382,15 +399,15 @@
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding IsIndexing}"/>
<TextBlock Text="Indexing..." FontSize="11"
<TextBlock Text="{x:Static lang:Resources.Label_Indexing}" FontSize="11"
VerticalAlignment="Center"
IsVisible="{Binding IsIndexing}"/>
<Button
Command="{Binding OpenModelsFolderCommand}">
<TextBlock Text="Models Folder"/>
<TextBlock Text="{x:Static lang:Resources.Label_ModelsFolder}"/>
</Button>
<DropDownButton
Content="Categories"
Content="{x:Static lang:Resources.Label_Categories}"
Margin="8"
VerticalAlignment="Center"
HorizontalAlignment="Right">

5
StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml

@ -5,6 +5,7 @@
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.ExceptionViewModel}"
x:DataType="dialogs:ExceptionViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550"
@ -21,7 +22,7 @@
Grid.Row="0"
Theme="{DynamicResource SubtitleTextBlockStyle}"
Margin="16"
Text="An unexpected error occured"
Text="{x:Static lang:Resources.Label_UnexpectedErrorOccurred}"
TextWrapping="Wrap"
VerticalAlignment="Top" />
@ -74,7 +75,7 @@
<Button
Name="ExitButton"
IsDefault="True"
Content="Exit Application"
Content="{x:Static lang:Resources.Action_ExitApplication}"
Click="ExitButton_OnClick"
Grid.Row="2"
HorizontalAlignment="Right"

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

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

5
StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml

@ -6,6 +6,7 @@
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="700"
x:DataType="dialogs:OneClickInstallViewModel"
d:DataContext="{x:Static designData:DesignData.OneClickInstallViewModel}"
@ -87,7 +88,7 @@
IsVisible="{Binding ShowInstallButton}"
HorizontalAlignment="Center">
<Button
Content="Install"
Content="{x:Static lang:Resources.Action_Install}"
Command="{Binding InstallCommand}"
FontSize="32"
HorizontalAlignment="Center"
@ -95,7 +96,7 @@
Margin="16"
Padding="16, 8, 16, 8" />
<Button Command="{Binding ToggleAdvancedModeCommand}"
Content="Skip first-time setup"
Content="{x:Static lang:Resources.Label_SkipSetup}"
FontSize="16"
HorizontalAlignment="Center"
Padding="16, 8, 16, 8"/>

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

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

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

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

13
StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml

@ -6,6 +6,7 @@
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.SelectDataDirectoryViewModel}"
x:DataType="dialogs:SelectDataDirectoryViewModel"
x:CompileBindings="True"
@ -26,7 +27,7 @@
Title="Welcome back! In this update, you can optionally choose a custom location to store all data. If you choose a new location, or opt to use Portable Mode, you'll be able to migrate your existing data on the next page."/>
<Label
Content="Data Directory"
Content="{x:Static lang:Resources.Label_DataDirectory}"
FontSize="13"
Margin="0,16,0,0" />
<Grid ColumnDefinitions="*,Auto">
@ -50,14 +51,14 @@
</Grid>
<TextBlock
Text="This is where the model checkpoints, LORAs, web UIs, settings, etc. will be installed."
Text="{x:Static lang:Resources.Label_DataDirectoryExplanation}"
TextWrapping="Wrap"
Foreground="LightGray"
FontSize="12"
Margin="0,8,0,0" />
<TextBlock
Text="You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience."
Text="{x:Static lang:Resources.Label_FatWarning}"
TextWrapping="Wrap"
Foreground="Yellow"
IsVisible="{Binding ShowFatWarning}"
@ -65,7 +66,7 @@
Margin="0,8,0,0" />
<CheckBox
Content="Portable Mode"
Content="{x:Static lang:Resources.Label_PortableMode}"
IsChecked="{Binding IsPortableMode, Mode=TwoWay}"
Margin="0,32,0,0" />
@ -75,7 +76,7 @@
FontSize="13"
Margin="0,8,0,0"
Padding="16"
Title="In Portable Mode, all data and settings will be stored in the same directory as the application. You will be able to move the application with its 'Data' folder to a different location or computer." />
Title="{x:Static lang:Resources.Label_PortableModeExplanation}" />
</StackPanel>
@ -94,7 +95,7 @@
</StackPanel>
<Button
Content="Continue"
Content="{x:Static lang:Resources.Action_Continue}"
IsDefault="True"
Command="{Binding OnPrimaryButtonClick}"
FontSize="16"

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

@ -10,6 +10,7 @@
xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static designData:DesignData.SelectModelVersionViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.SelectModelVersionDialog">
@ -64,13 +65,13 @@
<Button Command="{Binding PreviousImage}"
Margin="0,0,8,0"
IsEnabled="{Binding CanGoToPreviousImage}"
ToolTip.Tip="Previous Image"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousImage}"
ToolTip.Placement="Top">
<avalonia:Icon Value="fa-solid fa-caret-left"/>
</Button>
<Button Command="{Binding NextImage}"
IsEnabled="{Binding CanGoToNextImage}"
ToolTip.Tip="Next Image"
ToolTip.Tip="{x:Static lang:Resources.Label_NextImage}"
ToolTip.Placement="Top">
<avalonia:Icon Value="fa-solid fa-caret-right"/>
</Button>
@ -164,7 +165,7 @@
<Expander Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
Header="Model Description"
Header="{x:Static lang:Resources.Label_ModelDescription}"
ExpandDirection="Down"
Margin="8, 16">
<ScrollViewer MaxHeight="200">
@ -179,14 +180,14 @@
Margin="0,8,0,0"
HorizontalAlignment="Center">
<Button
Content="Cancel"
Content="{x:Static lang:Resources.Action_Cancel}"
HorizontalAlignment="Center"
Command="{Binding Cancel}"
Margin="8, 0" />
<Button
Margin="8, 0"
Content="Import"
Content="{x:Static lang:Resources.Action_Import}"
Command="{Binding Import}"
IsEnabled="{Binding IsImportEnabled}"
Classes="accent" />

3
StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml

@ -7,6 +7,7 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.UpdateViewModel}"
x:DataType="dialogs:UpdateViewModel"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="550"
@ -20,7 +21,7 @@
</TitleBar>-->
<TextBlock Grid.Row="0"
Text="A new version of Stability Matrix is available!"
Text="{x:Static lang:Resources.Label_NewVersionAvailable}"
HorizontalAlignment="Center"
FontWeight="Thin"
Margin="0,16,0,0"

11
StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml

@ -6,6 +6,7 @@
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.FirstLaunchSetupViewModel}"
x:DataType="vm:FirstLaunchSetupViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
@ -28,7 +29,7 @@
<Grid Margin="0,16,32,32" RowDefinitions="0.6*,0.4*">
<!-- Texts -->
<StackPanel Margin="32,64,0,0">
<TextBlock FontSize="24" Text="Let's get started" />
<TextBlock FontSize="24" Text="{x:Static lang:Resources.Label_LetsGetStarted}" />
<!-- Loading icon and text -->
<Grid Margin="0,8,0,0" ColumnDefinitions="0.15*,*">
<controls:RefreshBadge
@ -79,7 +80,7 @@
<TextBlock
VerticalAlignment="Top"
FontSize="14"
Text="I have read and agree to the"/>
Text="{x:Static lang:Resources.Label_ReadAndAgree}"/>
<ui:HyperlinkButton
VerticalAlignment="Top"
@ -87,7 +88,7 @@
Margin="1,0,0,0"
Classes="transparent"
NavigateUri="https://lykos.ai/matrix/license"
Content="License Agreement."
Content="{x:Static lang:Resources.Label_LicenseAgreement}"
Grid.Column="1" />
</Grid>
</CheckBox>
@ -102,11 +103,11 @@
<Button
Classes="danger"
Click="QuitButton_OnClick"
Content="Quit" />
Content="{x:Static lang:Resources.Action_Quit}" />
<Button
Classes="info"
Click="ContinueButton_OnClick"
Content="Continue"
Content="{x:Static lang:Resources.Action_Continue}"
IsEnabled="{Binding EulaAccepted, Mode=OneWay}" />
</UniformGrid>

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

@ -31,7 +31,8 @@ public partial class LaunchPageView : UserControlBase
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);
});

9
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -8,6 +8,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:base="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550"
d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}"
x:DataType="vm:MainWindowViewModel"
@ -79,7 +80,7 @@
<StackPanel>
<!-- Update button -->
<ui:NavigationViewItem Name="FooterUpdateItem"
Content="Update Available"
Content="{x:Static lang:Resources.Label_UpdateAvailable}"
IsVisible="{Binding IsUpdateAvailable}"
IsEnabled="{Binding IsUpdateAvailable}"
DataContext="{Binding UpdateViewModel}"
@ -91,7 +92,7 @@
<!-- Patreon -->
<ui:NavigationViewItem Name="PatreonPatreonItem"
Content="Become a Patron"
Content="{x:Static lang:Resources.Label_BecomeAPatron}"
Tapped="PatreonPatreonItem_OnTapped">
<ui:NavigationViewItem.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-patreon"/>
@ -100,7 +101,7 @@
<!-- Discord -->
<ui:NavigationViewItem Name="FooterDiscordItem"
Content="Discord Server"
Content="{x:Static lang:Resources.Label_JoinDiscord}"
Tapped="FooterDiscordItem_OnTapped">
<ui:NavigationViewItem.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-discord"/>
@ -109,7 +110,7 @@
<!-- Download flyout -->
<ui:NavigationViewItem Name="FooterDownloadItem"
Content="Downloads"
Content="{x:Static lang:Resources.Label_Downloads}"
IconSource="{Binding IconSource}"
DataContext="{Binding ProgressManagerViewModel}"
IsVisible="{Binding !!ProgressItems.Count}"

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

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

69
StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml

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

1
StabilityMatrix.Core/Database/ILiteDbContext.cs

@ -13,7 +13,6 @@ public interface ILiteDbContext : IDisposable
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);

77
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -23,16 +23,22 @@ public class LiteDbContext : ILiteDbContext
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)
IOptions<DebugOptions> debugOptions
)
{
this.logger = logger;
this.settingsManager = settingsManager;
@ -53,15 +59,20 @@ public class LiteDbContext : ILiteDbContext
try
{
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync(new ConnectionString()
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
);
}
}
@ -69,30 +80,43 @@ public class LiteDbContext : ILiteDbContext
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!
var version = await CivitModelVersions
.Query()
.Where(
mv =>
mv.Files!
.Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3))
.Any(hash => hash == hashBlake3)
)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
if (version is null) return (null, null);
if (version is null)
return (null, null);
var model = await CivitModels.Query()
var model = await CivitModels
.Query()
.Include(m => m.ModelVersions)
.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);
}
@ -100,7 +124,9 @@ public class LiteDbContext : ILiteDbContext
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;
@ -141,7 +167,8 @@ 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)
{
@ -162,9 +189,7 @@ public class LiteDbContext : ILiteDbContext
{
database.Dispose();
}
catch (ObjectDisposedException)
{
}
catch (ObjectDisposedException) { }
database = null;
}

40
StabilityMatrix.Core/Helper/EventManager.cs

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

65
StabilityMatrix.Core/Helper/SharedFolders.cs

@ -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,7 +86,8 @@ 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);
}
@ -89,10 +95,15 @@ public class SharedFolders : ISharedFolders
/// 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)
{
@ -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);
}
@ -161,7 +177,8 @@ public class SharedFolders : ISharedFolders
{
var destination = Path.GetFullPath(Path.Combine(installPath, relativePath));
// Delete the destination folder if it exists
if (!Directory.Exists(destination)) continue;
if (!Directory.Exists(destination))
continue;
Logger.Info($"Deleting junction target {destination}");
Directory.Delete(destination, false);
@ -174,19 +191,32 @@ public class SharedFolders : ISharedFolders
var packages = settingsManager.Settings.InstalledPackages;
foreach (var package in packages)
{
if (package.PackageName == null) continue;
if (package.PackageName == null)
continue;
var basePackage = packageFactory[package.PackageName];
if (basePackage == null) continue;
if (package.LibraryPath == null) continue;
if (basePackage == null)
continue;
if (package.LibraryPath == null)
continue;
try
{
basePackage.RemoveModelFolderLinks(package.FullPath).GetAwaiter().GetResult();
var sharedFolderMethod =
package.PreferredSharedFolderMethod
?? basePackage.RecommendedSharedFolderMethod;
basePackage
.RemoveModelFolderLinks(package.FullPath, sharedFolderMethod)
.GetAwaiter()
.GetResult();
}
catch (Exception e)
{
Logger.Warn("Failed to remove links for package {Package} " +
"({DisplayName}): {Message}", package.PackageName, package.DisplayName, e.Message);
Logger.Warn(
"Failed to remove links for package {Package} " + "({DisplayName}): {Message}",
package.PackageName,
package.DisplayName,
e.Message
);
}
}
}
@ -194,7 +224,8 @@ 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>();

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

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

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

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

8
StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs

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

29
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);
@ -31,8 +32,7 @@ public class FilePath : FileSystemPath, IPathObject
public string Name => Info.Name;
[JsonIgnore]
public string NameWithoutExtension
=> Path.GetFileNameWithoutExtension(Info.Name);
public string NameWithoutExtension => Path.GetFileNameWithoutExtension(Info.Name);
/// <summary>
/// Get the directory of the file.
@ -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,22 +53,20 @@ public class FilePath : FileSystemPath, IPathObject
}
}
public FilePath(string path) : base(path)
{
}
public FilePath(string path)
: base(path) { }
public FilePath(FileInfo fileInfo) : base(fileInfo.FullName)
public FilePath(FileInfo fileInfo)
: base(fileInfo.FullName)
{
_info = fileInfo;
}
public FilePath(FileSystemPath path) : base(path)
{
}
public FilePath(FileSystemPath path)
: base(path) { }
public FilePath(params string[] paths) : base(paths)
{
}
public FilePath(params string[] paths)
: base(paths) { }
public long GetSize()
{
@ -79,7 +76,8 @@ public class FilePath : FileSystemPath, IPathObject
public long GetSize(bool includeSymbolicLinks)
{
if (!includeSymbolicLinks && IsSymbolicLink) return 0;
if (!includeSymbolicLinks && IsSymbolicLink)
return 0;
return GetSize();
}
@ -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);
}

72
StabilityMatrix.Core/Models/InstalledPackage.cs

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

23
StabilityMatrix.Core/Models/InstalledPackageVersion.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1
StabilityMatrix.Core/Models/PackageVersion.cs

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

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

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

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

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

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

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

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

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

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

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

111
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,14 +30,44 @@ 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()
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
new LaunchOptionDefinition
{
Name = "Port",
Type = LaunchOptionType.String,
Description = "Sets the listen port",
Options = { "--port" }
},
new LaunchOptionDefinition
{
Name = "Share",
Type = LaunchOptionType.Bool,
Description = "Set whether to share on Gradio",
Options = { "--share" }
},
new LaunchOptionDefinition
{
Name = "Listen",
Type = LaunchOptionType.String,
Description = "Set the listen interface",
Options = { "--listen" }
},
LaunchOptionDefinition.Extras
};
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => new()
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
[SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" },
[SharedFolderType.Diffusers] = new[] { "models/diffusers" },
@ -50,49 +82,71 @@ public class Fooocus : BaseGitPackage
[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(IProgress<ProgressReport>? progress = null)
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await base.InstallPackage(progress).ConfigureAwait(false);
var venvRunner = await SetupVenv(InstallLocation, forceRecreate: true).ConfigureAwait(false);
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true));
var torchVersion = "cpu";
var gpus = HardwareHelper.IterGpuInfo().ToList();
var torchVersionStr = "cpu";
if (gpus.Any(g => g.IsNvidia))
{
torchVersion = "cu118";
}
else if (HardwareHelper.PreferRocm())
switch (torchVersion)
{
torchVersion = "rocm5.4.2";
case TorchVersion.Cuda:
torchVersionStr = "cu118";
break;
case TorchVersion.Rocm:
torchVersionStr = "rocm5.4.2";
break;
case TorchVersion.Cpu:
break;
default:
throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null);
}
await venvRunner
.PipInstall(
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersion}",
OnConsoleOutput).ConfigureAwait(false);
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}",
onConsoleOutput
)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing requirements...",
isIndeterminate: true));
await venvRunner.PipInstall("-r requirements_versions.txt", OnConsoleOutput)
progress?.Report(
new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)
);
await venvRunner
.PipInstall("-r requirements_versions.txt", onConsoleOutput)
.ConfigureAwait(false);
}
public override async Task RunPackage(string installedPackagePath, string command, string arguments)
public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
onConsoleOutput?.Invoke(s);
if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase))
{
@ -114,9 +168,6 @@ public class Fooocus : BaseGitPackage
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner?.RunDetached(
args.TrimEnd(),
HandleConsoleOutput,
HandleExit);
VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
}
}

175
StabilityMatrix.Core/Models/Packages/FooocusMre.cs

@ -0,0 +1,175 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
public class FooocusMre : BaseGitPackage
{
public FooocusMre(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
public override string Name => "Fooocus-MRE";
public override string DisplayName { get; set; } = "Fooocus-MRE";
public override string Author => "MoonRide303";
public override string Blurb =>
"Fooocus-MRE is an image generating software, enhanced variant of the original Fooocus dedicated for a bit more advanced users";
public override string LicenseType => "GPL-3.0";
public override string LicenseUrl =>
"https://github.com/MoonRide303/Fooocus-MRE/blob/moonride-main/LICENSE";
public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri =>
new(
"https://user-images.githubusercontent.com/130458190/265366059-ce430ea0-0995-4067-98dd-cef1d7dc1ab6.png"
);
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
new LaunchOptionDefinition
{
Name = "Port",
Type = LaunchOptionType.String,
Description = "Sets the listen port",
Options = { "--port" }
},
new LaunchOptionDefinition
{
Name = "Share",
Type = LaunchOptionType.Bool,
Description = "Set whether to share on Gradio",
Options = { "--share" }
},
new LaunchOptionDefinition
{
Name = "Listen",
Type = LaunchOptionType.String,
Description = "Set the listen interface",
Options = { "--listen" }
},
LaunchOptionDefinition.Extras
};
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
[SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" },
[SharedFolderType.Diffusers] = new[] { "models/diffusers" },
[SharedFolderType.Lora] = new[] { "models/loras" },
[SharedFolderType.CLIP] = new[] { "models/clip" },
[SharedFolderType.TextualInversion] = new[] { "models/embeddings" },
[SharedFolderType.VAE] = new[] { "models/vae" },
[SharedFolderType.ApproxVAE] = new[] { "models/vae_approx" },
[SharedFolderType.ControlNet] = new[] { "models/controlnet" },
[SharedFolderType.GLIGEN] = new[] { "models/gligen" },
[SharedFolderType.ESRGAN] = new[] { "models/upscale_models" },
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }
};
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm };
public override 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,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true));
var torchVersionStr = "cpu";
switch (torchVersion)
{
case TorchVersion.Cuda:
torchVersionStr = "cu118";
break;
case TorchVersion.Rocm:
torchVersionStr = "rocm5.4.2";
break;
case TorchVersion.Cpu:
break;
default:
throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null);
}
await venvRunner
.PipInstall(
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}",
onConsoleOutput
)
.ConfigureAwait(false);
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,
Action<ProcessOutput>? onConsoleOutput
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s)
{
onConsoleOutput?.Invoke(s);
if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase))
{
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
var match = regex.Match(s.Text);
if (match.Success)
{
WebUrl = match.Value;
}
OnStartupComplete(WebUrl);
}
}
void HandleExit(int i)
{
Debug.WriteLine($"Venv process exited with code {i}");
OnExit(i);
}
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
}
}

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

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

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

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

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

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

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

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

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

@ -19,8 +19,10 @@ public class VoltaML : BaseGitPackage
public override string Blurb => "Fast Stable Diffusion with support for AITemplate";
public override string 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
@ -30,21 +32,29 @@ public class VoltaML : BaseGitPackage
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()
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 IEnumerable<TorchVersion> AvailableTorchVersions => new[] { TorchVersion.None };
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
// https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L45
public override List<LaunchOptionDefinition> LaunchOptions => new List<LaunchOptionDefinition>
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{
new()
{
@ -123,27 +133,41 @@ public class VoltaML : BaseGitPackage
public override Task<string> GetLatestVersion() => Task.FromResult("main");
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null)
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await UnzipPackage(progress).ConfigureAwait(false);
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
// Setup venv
progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true));
await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation;
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true).ConfigureAwait(false);
// Install requirements
progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true));
progress?.Report(
new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)
);
await venvRunner
.PipInstall("rich packaging python-dotenv", OnConsoleOutput)
.PipInstall("rich packaging python-dotenv", onConsoleOutput)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false));
progress?.Report(
new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)
);
}
public override async Task RunPackage(string installedPackagePath, string command, string arguments)
public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
@ -153,7 +177,7 @@ public class VoltaML : BaseGitPackage
void HandleConsoleOutput(ProcessOutput s)
{
OnConsoleOutput(s);
onConsoleOutput?.Invoke(s);
if (s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase))
{

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save