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
      .github/workflows/release.yml
  2. 2
      .husky/task-runner.json
  3. 18
      CHANGELOG.md
  4. 10
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs
  5. 16
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs
  6. 8
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs
  7. 10
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs
  8. 52
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs
  9. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs
  10. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs
  11. 7
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs
  12. 12
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs
  13. 2
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs
  14. 13
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs
  15. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs
  16. 36
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs
  17. 30
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs
  18. 4
      StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs
  19. 7
      StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs
  20. 11
      StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs
  21. 3
      StabilityMatrix.Avalonia/App.axaml
  22. 14
      StabilityMatrix.Avalonia/App.axaml.cs
  23. 149
      StabilityMatrix.Avalonia/Assets.cs
  24. 132
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  25. 308
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  26. 15
      StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
  27. 4
      StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
  28. 549
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  29. 33
      StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx
  30. 183
      StabilityMatrix.Avalonia/Languages/Resources.resx
  31. 122
      StabilityMatrix.Avalonia/Program.cs
  32. 11
      StabilityMatrix.Avalonia/ViewModels/Base/ConsoleProgressViewModel.cs
  33. 26
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
  34. 3
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs
  35. 15
      StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs
  36. 372
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  37. 164
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  38. 349
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  39. 194
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  40. 199
      StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
  41. 3
      StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs
  42. 497
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
  43. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
  44. 123
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  45. 199
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
  46. 94
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
  47. 50
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  48. 78
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  49. 332
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  50. 62
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  51. 189
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  52. 67
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  53. 4
      StabilityMatrix.Avalonia/ViewModels/Progress/DownloadProgressItemViewModel.cs
  54. 78
      StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
  55. 6
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressItemViewModel.cs
  56. 72
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs
  57. 322
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  58. 31
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  59. 89
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  60. 5
      StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml
  61. 440
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  62. 5
      StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml
  63. 57
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
  64. 51
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs
  65. 13
      StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml
  66. 11
      StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
  67. 3
      StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml
  68. 11
      StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml
  69. 16
      StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs
  70. 11
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  71. 7
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  72. 69
      StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml
  73. 3
      StabilityMatrix.Core/Database/ILiteDbContext.cs
  74. 115
      StabilityMatrix.Core/Database/LiteDbContext.cs
  75. 44
      StabilityMatrix.Core/Helper/EventManager.cs
  76. 81
      StabilityMatrix.Core/Helper/SharedFolders.cs
  77. 7
      StabilityMatrix.Core/Models/Database/GitCommit.cs
  78. 7
      StabilityMatrix.Core/Models/Database/LocalModelFile.cs
  79. 8
      StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs
  80. 75
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  81. 110
      StabilityMatrix.Core/Models/InstalledPackage.cs
  82. 23
      StabilityMatrix.Core/Models/InstalledPackageVersion.cs
  83. 33
      StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs
  84. 27
      StabilityMatrix.Core/Models/PackageModification/DownloadPackageVersionStep.cs
  85. 14
      StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs
  86. 33
      StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs
  87. 43
      StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs
  88. 9
      StabilityMatrix.Core/Models/PackageModification/PackageStep.cs
  89. 24
      StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs
  90. 32
      StabilityMatrix.Core/Models/PackageModification/SetupModelFoldersStep.cs
  91. 44
      StabilityMatrix.Core/Models/PackageModification/SetupPrerequisitesStep.cs
  92. 1
      StabilityMatrix.Core/Models/PackageVersion.cs
  93. 306
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  94. 276
      StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
  95. 177
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  96. 310
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  97. 55
      StabilityMatrix.Core/Models/Packages/DankDiffusion.cs
  98. 151
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  99. 175
      StabilityMatrix.Core/Models/Packages/FooocusMre.cs
  100. 387
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  101. Some files were not shown because too many files have changed in this diff Show More

2
.github/workflows/release.yml

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

2
.husky/task-runner.json

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

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)

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

@ -8,8 +8,7 @@ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Controls;
public partial class LogViewerControl : UserControl
{
public LogViewerControl()
=> InitializeComponent();
public LogViewerControl() => InitializeComponent();
private ILogDataStoreImpl? vm;
private LogModel? item;
@ -17,7 +16,7 @@ public partial class LogViewerControl : UserControl
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is null)
return;
@ -45,8 +44,9 @@ public partial class LogViewerControl : UserControl
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
if (vm is null) return;
if (vm is null)
return;
vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged;
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3
StabilityMatrix.Avalonia/App.axaml

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

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

149
StabilityMatrix.Avalonia/Assets.cs

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

132
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

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

308
StabilityMatrix.Avalonia/DesignData/DesignData.cs

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

122
StabilityMatrix.Avalonia/Program.cs

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

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

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

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

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

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

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

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

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

372
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -51,48 +51,93 @@ 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<CivitModelType> AllModelTypes => Enum.GetValues(typeof(CivitModelType))
.Cast<CivitModelType>()
.Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0)
.OrderBy(t => t.ToString());
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))
.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,
IDownloadService downloadService,
ICivitApi civitApi,
IDownloadService downloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
ILiteDbContext liteDbContext,
INotificationService notificationService)
INotificationService notificationService
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
@ -117,27 +162,38 @@ 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})
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;
}
SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month;
SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated;
SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint;
SelectedBaseModelType = searchOptions?.SelectedBaseModelType ?? "All";
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()
{
Id = ObjectHash.GetMd5Guid(request),
InsertedAt = DateTimeOffset.UtcNow,
Request = request,
Items = models,
Metadata = modelsResponse.Metadata
});
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
@ -235,11 +322,11 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
UpdateResultsText();
}
}
/// <summary>
/// Updates model cards using api response object.
/// </summary>
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata)
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata)
{
if (models is null)
{
@ -266,26 +353,29 @@ 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);
};
return vm;
});
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);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
}
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
@ -304,14 +394,14 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
private async Task SearchModels()
{
var timer = Stopwatch.StartNew();
if (SearchQuery != previousSearchQuery)
{
// Reset page number
CurrentPageNumber = 1;
previousSearchQuery = SearchQuery;
}
// Build request
var modelRequest = new CivitModelsRequest
{
@ -334,10 +424,10 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
{
modelRequest.Query = SearchQuery;
}
if (SelectedModelType != CivitModelType.All)
{
modelRequest.Types = new[] {SelectedModelType};
modelRequest.Types = new[] { SelectedModelType };
}
if (SelectedBaseModelType != "All")
@ -347,9 +437,9 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
if (SortMode == CivitSortMode.Installed)
{
var connectedModels =
CheckpointFile.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
if (SelectedModelType != CivitModelType.All)
{
@ -358,24 +448,34 @@ 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,
};
}
// See if query is cached
var cachedQuery = await liteDbContext.CivitModelQueryCache
.IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
// If cached, update model cards
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
@ -395,7 +496,7 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
ShowMainLoadingSpinner = true;
await CivitModelQuery(modelRequest);
}
UpdateResultsText();
}
@ -406,17 +507,17 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
public void PreviousPage()
{
if (CurrentPageNumber == 1)
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
@ -429,46 +530,75 @@ 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();
}
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
? $"{allModelCards.Count} results hidden by filters"
: "No results found";
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 };
}

164
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,10 +88,11 @@ 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) ?? "";
var modelNameNoExt = Path.GetFileNameWithoutExtension((string?)FilePath);
var modelDir = Path.GetDirectoryName((string?)FilePath) ?? "";
return Path.Combine(modelDir, $"{modelNameNoExt}.cm-info.json");
}
@ -125,7 +136,7 @@ public partial class CheckpointFile : ViewModelBase
private async Task RenameAsync()
{
// Parent folder path
var parentPath = Path.GetDirectoryName((string?) FilePath) ?? "";
var parentPath = Path.GetDirectoryName((string?)FilePath) ?? "";
var textFields = new TextBoxField[]
{
@ -134,18 +145,18 @@ public partial class CheckpointFile : ViewModelBase
Label = "File name",
Validator = text =>
{
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 (string.IsNullOrWhiteSpace(text))
throw new DataValidationException("File name is required");
if (File.Exists(Path.Combine(parentPath, text)))
throw new DataValidationException("File name already exists");
},
Text = FileName
}
};
var dialog = DialogHelper.CreateTextEntryDialog("Rename Model", "", textFields);
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
var name = textFields[0].Text;
@ -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,22 +213,33 @@ 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
{
Title = Path.GetFileNameWithoutExtension(file),
FilePath = Path.Combine(directory, file),
};
var jsonPath = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.cm-info.json");
if (File.Exists(jsonPath))
var jsonPath = Path.Combine(
directory,
$"{Path.GetFileNameWithoutExtension(file)}.cm-info.json"
);
if (File.Exists(jsonPath))
{
var json = File.ReadAllText(jsonPath);
var connectedModelInfo = ConnectedModelInfo.FromJson(json);
@ -218,32 +247,51 @@ 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;
}
}
public static IEnumerable<CheckpointFile> GetAllCheckpointFiles(string modelsDirectory)
{
foreach (var file in Directory.EnumerateFiles(modelsDirectory, "*.*", SearchOption.AllDirectories))
foreach (
var file in Directory.EnumerateFiles(
modelsDirectory,
"*.*",
SearchOption.AllDirectories
)
)
{
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
{
Title = Path.GetFileNameWithoutExtension(file),
FilePath = file,
};
var jsonPath = Path.Combine(Path.GetDirectoryName(file) ?? "",
Path.GetFileNameWithoutExtension(file) + ".cm-info.json");
if (File.Exists(jsonPath))
var jsonPath = Path.Combine(
Path.GetDirectoryName(file) ?? "",
Path.GetFileNameWithoutExtension(file) + ".cm-info.json"
);
if (File.Exists(jsonPath))
{
var json = File.ReadAllText(jsonPath);
var connectedModelInfo = ConnectedModelInfo.FromJson(json);
@ -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))
@ -281,7 +338,7 @@ public partial class CheckpointFile : ViewModelBase
{
return CivitModelType.Checkpoint;
}
if (filePath.Contains(SharedFolderType.ControlNet.ToString()))
{
return CivitModelType.Controlnet;
@ -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();
}

349
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,9 +31,17 @@ 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;
/// <summary>
/// Absolute path to the folder.
/// </summary>
@ -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,64 +78,97 @@ 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;
public string TitleWithFilesCount =>
CheckpointFiles.Any() || SubFolders.Any(f => f.CheckpointFiles.Any())
? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})"
: Title;
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>
/// When title is set, set the category enabled state from settings.
/// </summary>
// 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);
FolderType = result ? type : new SharedFolderType();
IsCategoryEnabled = settingsManager.IsSharedFolderCategoryVisible(FolderType);
}
partial void OnSearchFilterChanged(string value)
{
if (string.IsNullOrEmpty(value))
{
DisplayedCheckpointFiles = CheckpointFiles;
}
else
foreach (var subFolder in SubFolders)
{
var filteredFiles = CheckpointFiles.Where(y =>
y.FileName.Contains(value, StringComparison.OrdinalIgnoreCase));
DisplayedCheckpointFiles = new AdvancedObservableList<CheckpointFile>(filteredFiles);
subFolder.SearchFilter = value;
}
checkpointFilesCache.Refresh();
}
/// <summary>
@ -130,13 +176,14 @@ 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);
}
}
private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(TitleWithFilesCount));
@ -168,7 +215,7 @@ public partial class CheckpointFolder : ViewModelBase
{
await ProcessRunner.OpenFolderBrowser(path);
}
[RelayCommand]
private async Task Delete()
{
@ -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>
@ -189,7 +238,7 @@ public partial class CheckpointFolder : ViewModelBase
TaskDialogButton.YesButton,
TaskDialogButton.NoButton
};
dialog.Closing += async (sender, e) =>
{
// We only want to use the deferral on the 'Yes' Button
@ -204,7 +253,7 @@ public partial class CheckpointFolder : ViewModelBase
{
await directory.DeleteAsync(true);
}
RemoveFromParentList();
deferral.Complete();
}
@ -214,12 +263,12 @@ public partial class CheckpointFolder : ViewModelBase
await dialog.ShowAsync(true);
}
[RelayCommand]
private async Task CreateSubFolder()
{
Dispatcher.UIThread.VerifyAccess();
var textBox = new TextBox();
var dialog = new ContentDialog
{
@ -235,52 +284,71 @@ 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)
{
Title = Path.GetFileName(subFolderPath),
DirectoryPath = subFolderPath,
FolderType = FolderType,
ParentFolder = this,
IsExpanded = 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}";
});
await FileTransfers.CopyFiles(copyPaths, progress);
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,27 +358,33 @@ 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);
// Start a task to query the model in background
var queryTask = Task.Run(async () =>
{
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
);
}
}
@ -330,29 +411,27 @@ public partial class CheckpointFolder : ViewModelBase
});
modelQueryTasks.Add(queryTask);
}
// Set progress to indeterminate
Progress.IsIndeterminate = true;
Progress.Text = "Checking connected model information";
// Wait for all model queries to finish
var modelQueryResults = await Task.WhenAll(modelQueryTasks);
var successCount = modelQueryResults.Count(r => r);
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,65 +448,107 @@ 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(_ =>
{
IsImportInProgress = false;
Progress.Value = 0;
Progress.IsIndeterminate = false;
Progress.Text = string.Empty;
});
Task.Delay(delay)
.ContinueWith(_ =>
{
IsImportInProgress = false;
Progress.Value = 0;
Progress.IsIndeterminate = false;
Progress.Text = string.Empty;
});
}
/// <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()),
_ => Task.Run(() =>
CheckpointFile.FromDirectoryIndex(DirectoryPath, progress).ToList())
});
return CheckpointFile.FromDirectoryIndex(DirectoryPath);
}
/// <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))
{
// Create subfolder
var subFolder = new CheckpointFolder(settingsManager,
downloadService, modelFinder, useCategoryVisibility: false)
// Get from cache or create new
if (SubFoldersCache.Lookup(folder) is { HasValue: true } result)
{
result.Value.BackgroundIndex();
}
else
{
Title = Path.GetFileName(folder),
DirectoryPath = folder,
FolderType = FolderType, // Inherit our folder type
ParentFolder = this,
IsExpanded = false, // Subfolders are collapsed by default
};
await subFolder.IndexAsync(progress);
SubFolders.Add(subFolder);
// Create subfolder
var subFolder = new CheckpointFolder(
settingsManager,
downloadService,
modelFinder,
notificationService,
useCategoryVisibility: false
)
{
Title = Path.GetFileName(folder),
DirectoryPath = folder,
FolderType = FolderType, // Inherit our folder type
ParentFolder = this,
IsExpanded = false, // Subfolders are collapsed by default
};
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
);
}
}

194
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,170 +31,177 @@ 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;
Logger.Info($"OnLoadedAsync in {sw.ElapsedMilliseconds}ms");
}
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnSearchFilterChanged(string value)
{
var sw = Stopwatch.StartNew();
if (string.IsNullOrWhiteSpace(SearchFilter))
foreach (var folder in CheckpointFolders)
{
DisplayedCheckpointFolders = new ObservableCollection<CheckpointFolder>(
CheckpointFolders.Select(x =>
{
x.SearchFilter = SearchFilter;
return x;
}));
sw.Stop();
Logger.Info($"OnSearchFilterChanged in {sw.ElapsedMilliseconds}ms");
return;
folder.SearchFilter = value;
}
sw.Restart();
var filteredFolders = CheckpointFolders
.Where(ContainsSearchFilter).ToList();
foreach (var folder in filteredFolders)
CheckpointFoldersCache.Refresh();
}
partial void OnShowConnectedModelImagesChanged(bool value)
{
if (
settingsManager.IsLibraryDirSet
&& value != settingsManager.Settings.ShowConnectedModelImages
)
{
folder.SearchFilter = SearchFilter;
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)) ||
// If no matching files were found in the current folder, check in all subfolders
folder.SubFolders.Any(subFolder => ContainsSearchFilter(subFolder));
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(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();
var folders = Directory.GetDirectories(modelsDirectory);
var sw = Stopwatch.StartNew();
// Index all folders
var indexTasks = folders.Select(async f =>
foreach (var folder in folders)
{
var checkpointFolder =
new CheckpointFolder(settingsManager, downloadService, modelFinder)
// Get from cache or create new
if (CheckpointFoldersCache.Lookup(folder) is { HasValue: true } result)
{
result.Value.Index();
}
else
{
var checkpointFolder = new CheckpointFolder(
settingsManager,
downloadService,
modelFinder,
notificationService
)
{
Title = Path.GetFileName(f),
DirectoryPath = f,
IsExpanded = true, // Top level folders expanded by default
Title = Path.GetFileName(folder),
DirectoryPath = folder,
IsExpanded = true // Top level folders expanded by default
};
await checkpointFolder.IndexAsync();
return checkpointFolder;
}).ToList();
checkpointFolder.Index();
CheckpointFoldersCache.AddOrUpdate(checkpointFolder);
}
}
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))
{
var filtered = CheckpointFolders
.Where(x => x.CheckpointFiles.Any(y => y.FileName.Contains(SearchFilter))).Select(
f =>
{
f.SearchFilter = SearchFilter;
return f;
});
DisplayedCheckpointFolders = new ObservableCollection<CheckpointFolder>(filtered);
}
else
{
DisplayedCheckpointFolders = CheckpointFolders;
}
Logger.Info($"IndexFolders in {sw.Elapsed.TotalMilliseconds:F1}ms");
}
[RelayCommand]

199
StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

332
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

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

62
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

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

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

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

67
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

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

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

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

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

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

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

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

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

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

322
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

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

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>

89
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 -->
<ToggleSwitch
OffContent="Import as Connected"
OnContent="Import as Connected"
Grid.Column="0"
IsChecked="{Binding IsImportAsConnected}"
Margin="24,8"
ToolTip.Tip="Search for connected metadata on new local imports" />
<StackPanel Grid.Column="0" Grid.Row="0">
<ToggleSwitch
OffContent="{x:Static lang:Resources.Label_ImportAsConnected}"
OnContent="{x:Static lang:Resources.Label_ImportAsConnected}"
IsChecked="{Binding IsImportAsConnected}"
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"

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

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

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>

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

@ -14,7 +14,7 @@ namespace StabilityMatrix.Avalonia.Views;
public partial class LaunchPageView : UserControlBase
{
private const int LineOffset = 5;
public LaunchPageView()
{
InitializeComponent();
@ -22,17 +22,18 @@ public partial class LaunchPageView : UserControlBase
if (editor is not null)
{
var options = new RegistryOptions(ThemeName.DarkPlus);
// Config hyperlinks
editor.TextArea.Options.EnableHyperlinks = true;
editor.TextArea.Options.RequireControlModifierForHyperlinkClick = false;
editor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Coral;
var textMate = editor.InstallTextMate(options);
var scope = options.GetScopeByLanguageId("log");
if (scope is null) throw new InvalidOperationException("Scope is null");
if (scope is null)
throw new InvalidOperationException("Scope is null");
textMate.SetGrammar(scope);
textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus));
}
@ -55,7 +56,8 @@ public partial class LaunchPageView : UserControlBase
Dispatcher.UIThread.Invoke(() =>
{
var editor = this.FindControl<TextEditor>("Console");
if (editor?.Document == null) return;
if (editor?.Document == null)
return;
var line = Math.Max(editor.Document.LineCount - LineOffset, 1);
editor.ScrollToLine(line);
});

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

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

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

69
StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml

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

3
StabilityMatrix.Core/Database/ILiteDbContext.cs

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

115
StabilityMatrix.Core/Database/LiteDbContext.cs

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

44
StabilityMatrix.Core/Helper/EventManager.cs

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

81
StabilityMatrix.Core/Helper/SharedFolders.cs

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

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

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

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

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

8
StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs

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

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

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

110
StabilityMatrix.Core/Models/InstalledPackage.cs

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

23
StabilityMatrix.Core/Models/InstalledPackageVersion.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1
StabilityMatrix.Core/Models/PackageVersion.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save