Browse Source

Merge branch 'inference-modules' of https://github.com/ionite34/StabilityMatrix

pull/333/head
JT 12 months ago
parent
commit
9286569a03
  1. 4
      .config/dotnet-tools.json
  2. 2
      .github/workflows/release.yml
  3. 47
      CHANGELOG.md
  4. 2
      StabilityMatrix.Avalonia/App.axaml
  5. 216
      StabilityMatrix.Avalonia/App.axaml.cs
  6. 49
      StabilityMatrix.Avalonia/Assets.cs
  7. 544
      StabilityMatrix.Avalonia/Assets/hf-packages.json
  8. 34
      StabilityMatrix.Avalonia/Assets/sitecustomize.py
  9. 312
      StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs
  10. 85
      StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs
  11. 113
      StabilityMatrix.Avalonia/Controls/Extensions/ShowDisabledTooltipExtension.cs
  12. 37
      StabilityMatrix.Avalonia/Controls/LineDashFrame.cs
  13. 129
      StabilityMatrix.Avalonia/Controls/ProgressRing.cs
  14. 124
      StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs
  15. 35
      StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs
  16. 36
      StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs
  17. 60
      StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs
  18. 14
      StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml
  19. 104
      StabilityMatrix.Avalonia/Controls/StackExpander.axaml
  20. 29
      StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs
  21. 22
      StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs
  22. 31
      StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs
  23. 41
      StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs
  24. 8
      StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs
  25. 253
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  26. 30
      StabilityMatrix.Avalonia/DesignData/MockMetadataImportService.cs
  27. 54
      StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs
  28. 2
      StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs
  29. 11
      StabilityMatrix.Avalonia/Helpers/ViewModelSerializer.cs
  30. 164
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  31. 3
      StabilityMatrix.Avalonia/Languages/Resources.es.resx
  32. 3
      StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx
  33. 3
      StabilityMatrix.Avalonia/Languages/Resources.it-it.resx
  34. 3
      StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx
  35. 56
      StabilityMatrix.Avalonia/Languages/Resources.resx
  36. 3
      StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx
  37. 3
      StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx
  38. 3
      StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx
  39. 46
      StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs
  40. 12
      StabilityMatrix.Avalonia/Models/HuggingFace/HuggingfaceItem.cs
  41. 4
      StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
  42. 55
      StabilityMatrix.Avalonia/Program.cs
  43. 5
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  44. 4
      StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml
  45. 46
      StabilityMatrix.Avalonia/Styles/ProgressRing.axaml
  46. 63
      StabilityMatrix.Avalonia/Styles/SplitButtonStyles.axaml
  47. 22
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs
  48. 139
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  49. 6
      StabilityMatrix.Avalonia/ViewModels/Base/TabViewModelBase.cs
  50. 10
      StabilityMatrix.Avalonia/ViewModels/Base/TaskDialogViewModelBase.cs
  51. 612
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  52. 193
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/HuggingFacePageViewModel.cs
  53. 654
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  54. 100
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  55. 151
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  56. 86
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  57. 45
      StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
  58. 47
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PropertyGridViewModel.cs
  59. 41
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  60. 8
      StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
  61. 52
      StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/CategoryViewModel.cs
  62. 25
      StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/HuggingfaceItemViewModel.cs
  63. 42
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
  64. 76
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs
  65. 24
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ModuleBase.cs
  66. 21
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/SaveImageModule.cs
  67. 61
      StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
  68. 4
      StabilityMatrix.Avalonia/ViewModels/Inference/StackEditableCardViewModel.cs
  69. 12
      StabilityMatrix.Avalonia/ViewModels/Inference/StackExpanderViewModel.cs
  70. 208
      StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
  71. 274
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  72. 5
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  73. 549
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  74. 29
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs
  75. 91
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  76. 35
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs
  77. 531
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml
  78. 41
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs
  79. 2
      StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml
  80. 20
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs
  81. 33
      StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml
  82. 13
      StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs
  83. 188
      StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml
  84. 13
      StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml.cs
  85. 148
      StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml
  86. 245
      StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
  87. 3
      StabilityMatrix.Avalonia/Views/SettingsPage.axaml
  88. 46
      StabilityMatrix.Core/Database/LiteDbContext.cs
  89. 72
      StabilityMatrix.Core/Extensions/EnumerableExtensions.cs
  90. 101
      StabilityMatrix.Core/Extensions/ObjectExtensions.cs
  91. 73
      StabilityMatrix.Core/Helper/ArchiveHelper.cs
  92. 43
      StabilityMatrix.Core/Helper/FileHash.cs
  93. 172
      StabilityMatrix.Core/Helper/HardwareHelper.cs
  94. 7
      StabilityMatrix.Core/Helper/HardwareInfo/CpuInfo.cs
  95. 31
      StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs
  96. 234
      StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs
  97. 10
      StabilityMatrix.Core/Helper/HardwareInfo/MemoryInfo.cs
  98. 9
      StabilityMatrix.Core/Helper/HardwareInfo/MemoryLevel.cs
  99. 19
      StabilityMatrix.Core/Helper/HardwareInfo/Win32MemoryStatusEx.cs
  100. 46
      StabilityMatrix.Core/Helper/ModelFinder.cs
  101. Some files were not shown because too many files have changed in this diff Show More

4
.config/dotnet-tools.json

@ -15,10 +15,10 @@
]
},
"csharpier": {
"version": "0.25.0",
"version": "0.26.4",
"commands": [
"dotnet-csharpier"
]
}
}
}
}

2
.github/workflows/release.yml

@ -209,8 +209,6 @@ jobs:
files: |
StabilityMatrix-win-x64.zip
StabilityMatrix-linux-x64.zip
StabilityMatrix-win-x64/StabilityMatrix.exe
StabilityMatrix-linux-x64/StabilityMatrix.AppImage
fail_on_unmatched_files: true
tag_name: v${{ github.event.inputs.version }}
body: ${{ steps.release_notes.outputs.release_notes }}

47
CHANGELOG.md

@ -5,6 +5,48 @@ 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.7.0-pre.4
### Added
#### Inference
- Added Image to Image project type
- Added Modular custom steps
- Use the plus button to add new steps (Hires Fix, Upscaler, and Save Image are currently available), and the edit button to enable removing or dragging steps to reorder them. This enables multi-pass Hires Fix, mixing different upscalers, and saving intermediate images at any point in the pipeline.
- Added Sampler addons
- Addons usually affect guidance like ControlNet, T2I, FreeU, and other addons to come. They apply to the individual sampler, so you can mix and match different ControlNets for Base and Hires Fix, or use the current output from a previous sampler as ControlNet guidance image for HighRes passes.
### Fixed
- Fixed Refiner model enabled state not saving to Inference project files
## v2.7.0-pre.3
### Added
- Added "Find Connected Metadata" options for root-level and file-level scans to the Checkpoints page
- Added "Update Existing Metadata" button to the Checkpoints page
- Added Hugging Face tab to the Model Browser
- Added the ability to type in a specific page number in the CivitAI Model Browser
### Changed
- Folder-level "Find Connected Metadata" now scans the selected folder and its subfolders
- Model Browser now split into "CivitAI" and "Hugging Face" tabs
### Fixed
- InvokeAI model links for T2I/IpAdapters now point to the correct folders
- Added extra checks to help prevent settings resetting in certain scenarios
## v2.7.0-pre.2
### Added
- Added System Information section to Settings
### Changed
- Moved Inference Settings to subpage
### Fixed
- Fixed crash when loading an empty settings file
- Improve Settings save and load performance with .NET 8 Source Generating Serialization
- Fixed ApplicationException during database shutdown
## v2.7.0-pre.1
### Fixed
- Fixed control character decoding that caused some progress bars to show as `\u2588`
- Fixed Python `rich` package's progress bars not showing in console
- Optimized ProgressRing animation bindings to reduce CPU usage
- Improved safety checks in custom control rendering to reduce potential graphical artifacts
- Improved console rendering safety with cursor line increment clamping, as potential fix for [#111](https://github.com/LykosAI/StabilityMatrix/issues/111)
## v2.7.0-dev.4
### Fixed
- Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters
@ -58,6 +100,11 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
## Changed
- Model Browser page has been redesigned, featuring more information like rating and download counts
## v2.6.7
### Fixed
- Fixed prerequisite install not unpacking due to improperly formatted 7z argument (Caused the "python310._pth FileNotFoundException")
- Fixed [#301](https://github.com/LykosAI/StabilityMatrix/issues/301) - Package updates failing silently because of a PortableGit error
## v2.6.6
### Fixed
- Fixed [#297](https://github.com/LykosAI/StabilityMatrix/issues/297) - Model browser LiteAsyncException occuring when fetching entries with unrecognized values from enum name changes

2
StabilityMatrix.Avalonia/App.axaml

@ -31,8 +31,6 @@
<idcr:ControlRecycling x:Key="ControlRecyclingKey" />
<x:Double x:Key="ContentDialogMaxWidth">700</x:Double>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">32</x:Double>
<SolidColorBrush x:Key="ToolTipBackground" Color="#1E1F22"/>
<SolidColorBrush x:Key="ToolTipForeground" Color="#9FBDC3"/>
<FontFamily x:Key="NotoSansJP">avares://StabilityMatrix.Avalonia/Assets/Fonts/NotoSansJP#Noto Sans JP</FontFamily>

216
StabilityMatrix.Avalonia/App.axaml.cs

@ -1,7 +1,3 @@
#if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions;
#endif
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -64,6 +60,10 @@ using StabilityMatrix.Core.Services;
using Application = Avalonia.Application;
using DrawingColor = System.Drawing.Color;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
#if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions;
#endif
namespace StabilityMatrix.Avalonia;
@ -77,8 +77,7 @@ public sealed class App : Application
public static TopLevel TopLevel => TopLevel.GetTopLevel(VisualRoot)!;
internal static bool IsHeadlessMode =>
TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB";
internal static bool IsHeadlessMode => TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB";
[NotNull]
public static IStorageProvider? StorageProvider { get; internal set; }
@ -114,7 +113,8 @@ public sealed class App : Application
public override void OnFrameworkInitializationCompleted()
{
// Remove DataAnnotations validation plugin since we're using INotifyDataErrorInfo from MvvmToolkit
var dataValidationPluginsToRemove = BindingPlugins.DataValidators
var dataValidationPluginsToRemove = BindingPlugins
.DataValidators
.OfType<DataAnnotationsValidationPlugin>()
.ToArray();
@ -158,19 +158,22 @@ public sealed class App : Application
DesktopLifetime.MainWindow = setupWindow;
setupWindow.ShowAsyncCts.Token.Register(() =>
{
if (setupWindow.Result == ContentDialogResult.Primary)
{
settingsManager.SetEulaAccepted();
ShowMainWindow();
DesktopLifetime.MainWindow.Show();
}
else
setupWindow
.ShowAsyncCts
.Token
.Register(() =>
{
Shutdown();
}
});
if (setupWindow.Result == ContentDialogResult.Primary)
{
settingsManager.SetEulaAccepted();
ShowMainWindow();
DesktopLifetime.MainWindow.Show();
}
else
{
Shutdown();
}
});
}
else
{
@ -219,9 +222,7 @@ public sealed class App : Application
mainWindow.Closing += (_, _) =>
{
var validWindowPosition = mainWindow.Screens.All.Any(
screen => screen.Bounds.Contains(mainWindow.Position)
);
var validWindowPosition = mainWindow.Screens.All.Any(screen => screen.Bounds.Contains(mainWindow.Position));
settingsManager.Transaction(
s =>
@ -301,10 +302,7 @@ public sealed class App : Application
services.AddSingleton<IDisposable>(p => p.GetRequiredService<LaunchPageViewModel>());
}
internal static void ConfigureDialogViewModels(
IServiceCollection services,
Type[] exportedTypes
)
internal static void ConfigureDialogViewModels(IServiceCollection services, Type[] exportedTypes)
{
// Dialog factory
services.AddSingleton<ServiceManager<ViewModelBase>>(provider =>
@ -312,17 +310,7 @@ public sealed class App : Application
var serviceManager = new ServiceManager<ViewModelBase>();
var serviceManagedTypes = exportedTypes
.Select(
t =>
new
{
t,
attributes = t.GetCustomAttributes(
typeof(ManagedServiceAttribute),
true
)
}
)
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) })
.Where(t1 => t1.attributes is { Length: > 0 })
.Select(t1 => t1.t)
.ToList();
@ -346,21 +334,18 @@ public sealed class App : Application
services.AddMessagePipe();
services.AddMessagePipeNamedPipeInterprocess("StabilityMatrix");
var exportedTypes = AppDomain.CurrentDomain
var exportedTypes = AppDomain
.CurrentDomain
.GetAssemblies()
.Where(a => a.FullName?.StartsWith("StabilityMatrix") == true)
.SelectMany(a => a.GetExportedTypes())
.ToArray();
var transientTypes = exportedTypes
.Select(
t =>
new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) }
)
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) })
.Where(
t1 =>
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attribute = (TransientAttribute)t1.attributes[0] });
@ -377,23 +362,12 @@ public sealed class App : Application
}
var singletonTypes = exportedTypes
.Select(
t =>
new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) }
)
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) })
.Where(
t1 =>
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(
t1 =>
new
{
Type = t1.t,
Attributes = t1.attributes.Cast<SingletonAttribute>().ToArray()
}
);
.Select(t1 => new { Type = t1.t, Attributes = t1.attributes.Cast<SingletonAttribute>().ToArray() });
foreach (var typePair in singletonTypes)
{
@ -425,9 +399,7 @@ public sealed class App : Application
// Rich presence
services.AddSingleton<IDiscordRichPresenceService, DiscordRichPresenceService>();
services.AddSingleton<IDisposable>(
provider => provider.GetRequiredService<IDiscordRichPresenceService>()
);
services.AddSingleton<IDisposable>(provider => provider.GetRequiredService<IDiscordRichPresenceService>());
Config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
@ -473,9 +445,7 @@ public sealed class App : Application
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter<CivitFileType>());
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter<CivitModelType>());
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter<CivitModelFormat>());
jsonSerializerOptions.Converters.Add(
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
);
jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
var defaultRefitSettings = new RefitSettings
@ -484,8 +454,7 @@ public sealed class App : Application
};
// Refit settings for IApiFactory
var defaultSystemTextJsonSettings =
SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions();
var defaultSystemTextJsonSettings = SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions();
defaultSystemTextJsonSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
var apiFactoryRefitSettings = new RefitSettings
{
@ -559,27 +528,19 @@ public sealed class App : Application
c.BaseAddress = new Uri("https://stableauthentication.azurewebsites.net");
c.Timeout = TimeSpan.FromSeconds(15);
})
.ConfigurePrimaryHttpMessageHandler(
() => new HttpClientHandler { AllowAutoRedirect = false }
)
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false })
.AddPolicyHandler(retryPolicy)
.AddHttpMessageHandler(
serviceProvider =>
new TokenAuthHeaderHandler(
serviceProvider.GetRequiredService<LykosAuthTokenProvider>()
)
new TokenAuthHeaderHandler(serviceProvider.GetRequiredService<LykosAuthTokenProvider>())
);
// Add Refit client managers
services
.AddHttpClient("A3Client")
.AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));
services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));
services
.AddHttpClient("DontFollowRedirects")
.ConfigurePrimaryHttpMessageHandler(
() => new HttpClientHandler { AllowAutoRedirect = false }
)
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false })
.AddPolicyHandler(retryPolicy);
/*services.AddHttpClient("IComfyApi")
@ -609,11 +570,7 @@ public sealed class App : Application
#if DEBUG
builder.AddNLog(
ConfigureLogging(),
new NLogProviderOptions
{
IgnoreEmptyEventId = false,
CaptureEventId = EventIdCaptureType.Legacy
}
new NLogProviderOptions { IgnoreEmptyEventId = false, CaptureEventId = EventIdCaptureType.Legacy }
);
#else
builder.AddNLog(ConfigureLogging());
@ -646,9 +603,7 @@ public sealed class App : Application
var settingsManager = Services.GetRequiredService<ISettingsManager>();
// If RemoveFolderLinksOnShutdown is set, delete all package junctions
if (
settingsManager is { IsLibraryDirSet: true, Settings.RemoveFolderLinksOnShutdown: true }
)
if (settingsManager is { IsLibraryDirSet: true, Settings.RemoveFolderLinksOnShutdown: true })
{
var sharedFolders = Services.GetRequiredService<ISharedFolders>();
sharedFolders.RemoveLinksForAllPackages();
@ -690,13 +645,10 @@ public sealed class App : Application
.WriteTo(
new FileTarget
{
Layout =
"${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
ArchiveOldFileOnStartup = true,
FileName =
"${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log",
ArchiveFileName =
"${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log",
ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
ArchiveNumbering = ArchiveNumberingMode.Rolling,
MaxArchiveFiles = 2
}
@ -709,8 +661,11 @@ public sealed class App : Application
builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn);
// Disable console trace logging by default
builder.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel").WriteToNil(NLog.LogLevel.Debug);
// Disable LoadableViewModelBase trace logging by default
builder
.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel")
.ForLogger("StabilityMatrix.Avalonia.ViewModels.Base.LoadableViewModelBase")
.WriteToNil(NLog.LogLevel.Debug);
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget);
@ -727,18 +682,20 @@ public sealed class App : Application
// Sentry
if (SentrySdk.IsEnabled)
{
LogManager.Configuration.AddSentry(o =>
{
o.InitializeSdk = false;
o.Layout = "${message}";
o.ShutdownTimeoutSeconds = 5;
o.IncludeEventDataOnBreadcrumbs = true;
o.BreadcrumbLayout = "${logger}: ${message}";
// Debug and higher are stored as breadcrumbs (default is Info)
o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug;
// Error and higher is sent as event (default is Error)
o.MinimumEventLevel = NLog.LogLevel.Error;
});
LogManager
.Configuration
.AddSentry(o =>
{
o.InitializeSdk = false;
o.Layout = "${message}";
o.ShutdownTimeoutSeconds = 5;
o.IncludeEventDataOnBreadcrumbs = true;
o.BreadcrumbLayout = "${logger}: ${message}";
// Debug and higher are stored as breadcrumbs (default is Info)
o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug;
// Error and higher is sent as event (default is Error)
o.MinimumEventLevel = NLog.LogLevel.Error;
});
}
LogManager.ReconfigExistingLoggers();
@ -777,36 +734,34 @@ public sealed class App : Application
results.Add(ms);
}
Dispatcher.UIThread.InvokeAsync(async () =>
{
var dest = await StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions()
{
SuggestedFileName = "screenshot.png",
ShowOverwritePrompt = true
}
);
if (dest?.TryGetLocalPath() is { } localPath)
Dispatcher
.UIThread
.InvokeAsync(async () =>
{
var localFile = new FilePath(localPath);
foreach (var (i, stream) in results.Enumerate())
var dest = await StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true }
);
if (dest?.TryGetLocalPath() is { } localPath)
{
var name = localFile.NameWithoutExtension;
if (results.Count > 1)
var localFile = new FilePath(localPath);
foreach (var (i, stream) in results.Enumerate())
{
name += $"_{i + 1}";
}
var name = localFile.NameWithoutExtension;
if (results.Count > 1)
{
name += $"_{i + 1}";
}
localFile = localFile.Directory!.JoinFile(name + ".png");
localFile.Create();
localFile = localFile.Directory!.JoinFile(name + ".png");
localFile.Create();
await using var fileStream = localFile.Info.OpenWrite();
stream.Seek(0, SeekOrigin.Begin);
await stream.CopyToAsync(fileStream);
await using var fileStream = localFile.Info.OpenWrite();
stream.Seek(0, SeekOrigin.Begin);
await stream.CopyToAsync(fileStream);
}
}
}
});
});
}
[Conditional("DEBUG")]
@ -822,8 +777,7 @@ public sealed class App : Application
{
#if DEBUG
setupBuilder.SetupExtensions(
extensionBuilder =>
extensionBuilder.RegisterTarget<DataStoreLoggerTarget>("DataStoreLogger")
extensionBuilder => extensionBuilder.RegisterTarget<DataStoreLoggerTarget>("DataStoreLogger")
);
#endif
}

49
StabilityMatrix.Avalonia/Assets.cs

@ -11,20 +11,16 @@ namespace StabilityMatrix.Avalonia;
internal static class Assets
{
public static AvaloniaResource AppIcon { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico");
public static AvaloniaResource AppIcon { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico");
public static AvaloniaResource AppIconPng { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/Icon.png");
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 Uri NoImage { get; } = new("avares://StabilityMatrix.Avalonia/Assets/noimage.png");
public static AvaloniaResource LicensesJson =>
new("avares://StabilityMatrix.Avalonia/Assets/licenses.json");
public static AvaloniaResource LicensesJson => new("avares://StabilityMatrix.Avalonia/Assets/licenses.json");
public static AvaloniaResource ImagePromptLanguageJson =>
new("avares://StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json");
@ -32,6 +28,8 @@ internal static class Assets
public static AvaloniaResource ThemeMatrixDarkJson =>
new("avares://StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json");
public static AvaloniaResource HfPackagesJson => new("avares://StabilityMatrix.Avalonia/Assets/hf-packages.json");
private const UnixFileMode Unix755 =
UnixFileMode.UserRead
| UnixFileMode.UserWrite
@ -46,23 +44,14 @@ internal static class Assets
[SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipExecutable =>
Compat.Switch(
(
PlatformKind.Windows,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")
),
(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
)
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", Unix755)
),
(
PlatformKind.MacOS | PlatformKind.Arm,
new AvaloniaResource(
"avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz",
Unix755
)
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", Unix755)
)
);
@ -73,21 +62,15 @@ internal static class Assets
Compat.Switch(
(
PlatformKind.Windows,
new AvaloniaResource(
"avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt"
)
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"
)
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"
)
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt")
)
);
@ -111,9 +94,7 @@ internal static class Assets
PlatformKind.Windows | PlatformKind.X64,
new RemoteResource
{
Url = new Uri(
"https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"
),
Url = new Uri("https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"),
HashSha256 = "608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d"
}
),
@ -167,9 +148,7 @@ internal static class Assets
/// <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);

544
StabilityMatrix.Avalonia/Assets/hf-packages.json

@ -0,0 +1,544 @@
[
{
"ModelCategory": "BaseModel",
"ModelName": "Stable Diffusion 1.5",
"RepositoryPath": "runwayml/stable-diffusion-v1-5",
"Files": [
"v1-5-pruned-emaonly.safetensors"
],
"LicenseType": "CreativeML Open RAIL-M"
},
{
"ModelCategory": "BaseModel",
"ModelName": "Stable Diffusion 2.1",
"RepositoryPath": "stabilityai/stable-diffusion-2-1",
"Files": [
"v2-1_768-ema-pruned.safetensors"
],
"LicenseType": "Open RAIL++"
},
{
"ModelCategory": "BaseModel",
"ModelName": "Stable Diffusion XL (Base)",
"RepositoryPath": "stabilityai/stable-diffusion-xl-base-1.0",
"Files": [
"sd_xl_base_1.0_0.9vae.safetensors",
"sd_xl_offset_example-lora_1.0.safetensors"
],
"LicenseType": "Open RAIL++",
"LicensePath": "LICENSE.md"
},
{
"ModelCategory": "BaseModel",
"ModelName": "Stable Diffusion XL (Refiner)",
"RepositoryPath": "stabilityai/stable-diffusion-xl-refiner-1.0",
"Files": [
"sd_xl_refiner_1.0_0.9vae.safetensors"
],
"LicenseType": "Open RAIL++",
"LicensePath": "LICENSE.md"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Canny",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_canny.pth",
"control_v11p_sd15_canny.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Depth",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11f1p_sd15_depth.pth",
"control_v11f1p_sd15_depth.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "MLSD",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_mlsd.pth",
"control_v11p_sd15_mlsd.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Inpaint",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_inpaint.pth",
"control_v11p_sd15_inpaint.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "IP2P",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11e_sd15_ip2p.pth",
"control_v11e_sd15_ip2p.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Tile",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11f1e_sd15_tile.pth",
"control_v11f1e_sd15_tile.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "OpenPose",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_openpose.pth",
"control_v11p_sd15_openpose.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "LineArt",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_lineart.pth",
"control_v11p_sd15_lineart.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "LineArt Anime",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15s2_lineart_anime.pth",
"control_v11p_sd15s2_lineart_anime.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "NormalBae",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_normalbae.pth",
"control_v11p_sd15_normalbae.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Seg",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_seg.pth",
"control_v11p_sd15_seg.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Soft Edge",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_softedge.pth",
"control_v11p_sd15_softedge.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Scribble",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11p_sd15_scribble.pth",
"control_v11p_sd15_scribble.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Shuffle",
"RepositoryPath": "lllyasviel/ControlNet-v1-1",
"Files": [
"control_v11e_sd15_shuffle.pth",
"control_v11e_sd15_shuffle.yaml"
],
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "Canny",
"RepositoryPath": "lllyasviel/control_v11p_sd15_canny",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "canny",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "Depth",
"RepositoryPath": "lllyasviel/control_v11f1p_sd15_depth",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "depth",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "MLSD",
"RepositoryPath": "lllyasviel/control_v11p_sd15_mlsd",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "MLSD",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "Inpaint",
"RepositoryPath": "lllyasviel/control_v11p_sd15_inpaint",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "inpaint",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "IP2P",
"RepositoryPath": "lllyasviel/control_v11e_sd15_ip2p",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "ip2p",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "Tile",
"RepositoryPath": "lllyasviel/control_v11f1e_sd15_tile",
"Files": [
"diffusion_pytorch_model.bin",
"config.json"
],
"Subfolder": "tile",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "OpenPose",
"RepositoryPath": "lllyasviel/control_v11p_sd15_openpose",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "openpose",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "LineArt",
"RepositoryPath": "lllyasviel/control_v11p_sd15_lineart",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "lineart",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "LineArt Anime",
"RepositoryPath": "lllyasviel/control_v11p_sd15s2_lineart_anime",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "lineart_anime",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "NormalBae",
"RepositoryPath": "lllyasviel/control_v11p_sd15_normalbae",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "normalbae",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "Seg",
"RepositoryPath": "lllyasviel/control_v11p_sd15_seg",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "seg",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "Soft Edge",
"RepositoryPath": "lllyasviel/control_v11p_sd15_softedge",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "softedge",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "Scribble",
"RepositoryPath": "lllyasviel/control_v11p_sd15_scribble",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "scribble",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersControlNet",
"ModelName": "Shuffle",
"RepositoryPath": "lllyasviel/control_v11e_sd15_shuffle",
"Files": [
"diffusion_pytorch_model.safetensors",
"config.json"
],
"Subfolder": "shuffle",
"LicenseType": "Open RAIL"
},
{
"ModelCategory": "DiffusersClipVision",
"ModelName": "IP Adapter Encoder",
"RepositoryPath": "InvokeAI/ip_adapter_sd_image_encoder",
"Files": [
"config.json",
"model.safetensors"
],
"Subfolder": "ip_adapter_sd_image_encoder",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersClipVision",
"ModelName": "IP Adapter Encoder (SDXL)",
"RepositoryPath": "InvokeAI/ip_adapter_sdxl_image_encoder",
"Files": [
"config.json",
"model.safetensors"
],
"Subfolder": "ip_adapter_sd_image_encoder",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersIpAdapter",
"ModelName": "SD 1.5 Adapter",
"RepositoryPath": "InvokeAI/ip_adapter_sd15",
"Files": [
"image_encoder.txt",
"ip_adapter.bin"
],
"Subfolder": "ip_adapter_sd15",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersIpAdapter",
"ModelName": "SD 1.5 Light Adapter",
"RepositoryPath": "InvokeAI/ip_adapter_sd15_light",
"Files": [
"image_encoder.txt",
"ip_adapter.bin"
],
"Subfolder": "ip_adapter_sd15_light",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersIpAdapter",
"ModelName": "SD 1.5 Plus Adapter",
"RepositoryPath": "InvokeAI/ip_adapter_plus_sd15",
"Files": [
"image_encoder.txt",
"ip_adapter.bin"
],
"Subfolder": "ip_adapter_plus_sd15",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersIpAdapter",
"ModelName": "SD 1.5 Face Plus Adapter",
"RepositoryPath": "InvokeAI/ip_adapter_plus_face_sd15",
"Files": [
"image_encoder.txt",
"ip_adapter.bin"
],
"Subfolder": "ip_adapter_plus_face_sd15",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersIpAdapterXl",
"ModelName": "SDXL Adapter",
"RepositoryPath": "InvokeAI/ip_adapter_sdxl",
"Files": [
"image_encoder.txt",
"ip_adapter.bin"
],
"Subfolder": "ip_adapter_sdxl",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersIpAdapterXl",
"ModelName": "SDXL Plus Adapter",
"RepositoryPath": "InvokeAI/ip-adapter-plus_sdxl_vit-h",
"Files": [
"image_encoder.txt",
"ip_adapter.bin"
],
"Subfolder": "ip_adapter_plus_sdxl_vit-h",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersIpAdapterXl",
"ModelName": "SDXL Face Plus Adapter",
"RepositoryPath": "InvokeAI/ip-adapter-plus-face_sdxl_vit-h",
"Files": [
"image_encoder.txt",
"ip_adapter.bin"
],
"Subfolder": "ip_adapter_plus_face_sdxl_vit-h",
"LicenseType": "CreativeML Open RAIL-M"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "Sketch",
"RepositoryPath": "TencentARC/t2iadapter_sketch_sd15v2",
"Files": [
"config.json",
"diffusion_pytorch_model.bin"
],
"Subfolder": "t2iadapter_sketch_sd15v2",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "Depth",
"RepositoryPath": "TencentARC/t2iadapter_depth_sd15v2",
"Files": [
"config.json",
"diffusion_pytorch_model.bin"
],
"Subfolder": "t2iadapter_depth_sd15v2",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "Canny",
"RepositoryPath": "TencentARC/t2iadapter_canny_sd15v2",
"Files": [
"config.json",
"diffusion_pytorch_model.bin"
],
"Subfolder": "t2iadapter_canny_sd15v2",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "Depth-Zoe",
"RepositoryPath": "TencentARC/t2iadapter_zoedepth_sd15v1",
"Files": [
"config.json",
"diffusion_pytorch_model.bin"
],
"Subfolder": "t2iadapter_zoedepth_sd15v1",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "Sketch (SDXL)",
"RepositoryPath": "TencentARC/t2i-adapter-sketch-sdxl-1.0",
"Files": [
"config.json",
"diffusion_pytorch_model.safetensors"
],
"Subfolder": "t2i-adapter-sketch-sdxl-1.0",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "Depth-Zoe (SDXL)",
"RepositoryPath": "TencentARC/t2i-adapter-depth-zoe-sdxl-1.0",
"Files": [
"config.json",
"diffusion_pytorch_model.safetensors"
],
"Subfolder": "t2i-adapter-depth-zoe-sdxl-1.0",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "OpenPose (SDXL)",
"RepositoryPath": "TencentARC/t2i-adapter-openpose-sdxl-1.0",
"Files": [
"config.json",
"diffusion_pytorch_model.safetensors"
],
"Subfolder": "t2i-adapter-openpose-sdxl-1.0",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "Depth-MiDaS (SDXL)",
"RepositoryPath": "TencentARC/t2i-adapter-depth-midas-sdxl-1.0",
"Files": [
"config.json",
"diffusion_pytorch_model.safetensors"
],
"Subfolder": "t2i-adapter-depth-midas-sdxl-1.0",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "LineArt (SDXL)",
"RepositoryPath": "TencentARC/t2i-adapter-lineart-sdxl-1.0",
"Files": [
"config.json",
"diffusion_pytorch_model.safetensors"
],
"Subfolder": "t2i-adapter-lineart-sdxl-1.0",
"LicenseType": "Apache 2.0"
},
{
"ModelCategory": "DiffusersT2IAdapter",
"ModelName": "Canny (SDXL)",
"RepositoryPath": "TencentARC/t2i-adapter-canny-sdxl-1.0",
"Files": [
"config.json",
"diffusion_pytorch_model.safetensors"
],
"Subfolder": "t2i-adapter-canny-sdxl-1.0",
"LicenseType": "Apache 2.0"
}
]

34
StabilityMatrix.Avalonia/Assets/sitecustomize.py

@ -46,7 +46,41 @@ def audit(event: str, *args):
# Reconfigure stdout to UTF-8
# noinspection PyUnresolvedReferences
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# Install the audit hook
sys.addaudithook(audit)
# Patch Rich terminal detection
def _patch_rich_console():
try:
from rich import console
class _Console(console.Console):
@property
def is_terminal(self) -> bool:
return True
console.Console = _Console
except ImportError:
pass
except Exception as e:
print("[sitecustomize error]:", e)
_patch_rich_console()
# Patch tqdm to use stdout instead of stderr
def _patch_tqdm():
try:
import sys
from tqdm import std
sys.stderr = sys.stdout
except ImportError:
pass
except Exception as e:
print("[sitecustomize error]:", e)
_patch_tqdm()

312
StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs

@ -54,11 +54,7 @@ public class AdvancedImageBox : TemplatedControl
remove => _propertyChanged -= value;
}
protected bool RaiseAndSetIfChanged<T>(
ref T field,
T value,
[CallerMemberName] string? propertyName = null
)
protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
@ -335,9 +331,7 @@ public class AdvancedImageBox : TemplatedControl
if (index < Count - 1)
index++;
return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel
? constrainZoomLevel
: this[index];
return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel ? constrainZoomLevel : this[index];
}
/// <summary>
@ -352,9 +346,7 @@ public class AdvancedImageBox : TemplatedControl
if (index > 0)
index--;
return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel
? constrainZoomLevel
: this[index];
return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel ? constrainZoomLevel : this[index];
}
/// <summary>
@ -525,11 +517,10 @@ public class AdvancedImageBox : TemplatedControl
#endregion
#region Properties
public static readonly DirectProperty<AdvancedImageBox, bool> CanRenderProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>(
nameof(CanRender),
o => o.CanRender
);
public static readonly DirectProperty<AdvancedImageBox, bool> CanRenderProperty = AvaloniaProperty.RegisterDirect<
AdvancedImageBox,
bool
>(nameof(CanRender), o => o.CanRender);
/// <summary>
/// Gets or sets if control can render the image
@ -560,11 +551,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(GridCellSizeProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> GridColorProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(GridColor),
SolidColorBrush.Parse("#181818")
);
public static readonly StyledProperty<ISolidColorBrush> GridColorProperty = AvaloniaProperty.Register<
AdvancedImageBox,
ISolidColorBrush
>(nameof(GridColor), SolidColorBrush.Parse("#181818"));
/// <summary>
/// Gets or sets the color used to create the checkerboard style background
@ -575,11 +565,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(GridColorProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> GridColorAlternateProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(GridColorAlternate),
SolidColorBrush.Parse("#252525")
);
public static readonly StyledProperty<ISolidColorBrush> GridColorAlternateProperty = AvaloniaProperty.Register<
AdvancedImageBox,
ISolidColorBrush
>(nameof(GridColorAlternate), SolidColorBrush.Parse("#252525"));
/// <summary>
/// Gets or sets the color used to create the checkerboard style background
@ -606,7 +595,8 @@ public class AdvancedImageBox : TemplatedControl
{
var loader = ImageLoader.AsyncImageLoader;
Dispatcher.UIThread
Dispatcher
.UIThread
.InvokeAsync(async () =>
{
Image = await loader.ProvideImageAsync(value);
@ -616,10 +606,9 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly StyledProperty<Bitmap?> ImageProperty = AvaloniaProperty.Register<
AdvancedImageBox,
Bitmap?
>(nameof(Image));
public static readonly StyledProperty<Bitmap?> ImageProperty = AvaloniaProperty.Register<AdvancedImageBox, Bitmap?>(
nameof(Image)
);
/// <summary>
/// Gets or sets the image to be displayed
@ -656,8 +645,7 @@ public class AdvancedImageBox : TemplatedControl
{
offsetBackup = Offset;
var zoomFactorScale =
(float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage);
var zoomFactorScale = (float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage);
var imageScale = newImage.Size / oldImage.Size;
Debug.WriteLine($"Image scale: {imageScale}");
@ -731,8 +719,10 @@ public class AdvancedImageBox : TemplatedControl
public bool HaveTrackerImage => _trackerImage is not null;
public static readonly StyledProperty<bool> TrackerImageAutoZoomProperty =
AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(TrackerImageAutoZoom), true);
public static readonly StyledProperty<bool> TrackerImageAutoZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(TrackerImageAutoZoom), true);
/// <summary>
/// Gets or sets if the tracker image will be scaled to the current zoom
@ -743,8 +733,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(TrackerImageAutoZoomProperty, value);
}
public static readonly StyledProperty<bool> IsTrackerImageEnabledProperty =
AvaloniaProperty.Register<AdvancedImageBox, bool>("IsTrackerImageEnabled");
public static readonly StyledProperty<bool> IsTrackerImageEnabledProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>("IsTrackerImageEnabled");
public bool IsTrackerImageEnabled
{
@ -776,10 +768,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly StyledProperty<bool> ShowGridProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(ShowGrid), true);
public static readonly StyledProperty<bool> ShowGridProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
nameof(ShowGrid),
true
);
/// <summary>
/// Gets or sets the grid visibility when reach high zoom levels
@ -791,10 +783,7 @@ public class AdvancedImageBox : TemplatedControl
}
public static readonly DirectProperty<AdvancedImageBox, Point> PointerPositionProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, Point>(
nameof(PointerPosition),
o => o.PointerPosition
);
AvaloniaProperty.RegisterDirect<AdvancedImageBox, Point>(nameof(PointerPosition), o => o.PointerPosition);
/// <summary>
/// Gets the current pointer position
@ -805,11 +794,10 @@ public class AdvancedImageBox : TemplatedControl
private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value);
}
public static readonly DirectProperty<AdvancedImageBox, bool> IsPanningProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>(
nameof(IsPanning),
o => o.IsPanning
);
public static readonly DirectProperty<AdvancedImageBox, bool> IsPanningProperty = AvaloniaProperty.RegisterDirect<
AdvancedImageBox,
bool
>(nameof(IsPanning), o => o.IsPanning);
/// <summary>
/// Gets if control is currently panning
@ -836,11 +824,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly DirectProperty<AdvancedImageBox, bool> IsSelectingProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>(
nameof(IsSelecting),
o => o.IsSelecting
);
public static readonly DirectProperty<AdvancedImageBox, bool> IsSelectingProperty = AvaloniaProperty.RegisterDirect<
AdvancedImageBox,
bool
>(nameof(IsSelecting), o => o.IsSelecting);
/// <summary>
/// Gets if control is currently selecting a ROI
@ -863,10 +850,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly StyledProperty<bool> AutoPanProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(AutoPan), true);
public static readonly StyledProperty<bool> AutoPanProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
nameof(AutoPan),
true
);
/// <summary>
/// Gets or sets if the control can pan with the mouse
@ -877,11 +864,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(AutoPanProperty, value);
}
public static readonly StyledProperty<MouseButtons> PanWithMouseButtonsProperty =
AvaloniaProperty.Register<AdvancedImageBox, MouseButtons>(
nameof(PanWithMouseButtons),
MouseButtons.LeftButton | MouseButtons.MiddleButton
);
public static readonly StyledProperty<MouseButtons> PanWithMouseButtonsProperty = AvaloniaProperty.Register<
AdvancedImageBox,
MouseButtons
>(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton);
/// <summary>
/// Gets or sets the mouse buttons to pan the image
@ -906,11 +892,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PanWithArrowsProperty, value);
}
public static readonly StyledProperty<MouseButtons> SelectWithMouseButtonsProperty =
AvaloniaProperty.Register<AdvancedImageBox, MouseButtons>(
nameof(SelectWithMouseButtons),
MouseButtons.LeftButton
);
public static readonly StyledProperty<MouseButtons> SelectWithMouseButtonsProperty = AvaloniaProperty.Register<
AdvancedImageBox,
MouseButtons
>(nameof(SelectWithMouseButtons), MouseButtons.LeftButton);
/// <summary>
/// Gets or sets the mouse buttons to select a region on image
@ -935,10 +920,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(InvertMousePanProperty, value);
}
public static readonly StyledProperty<bool> AutoCenterProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(AutoCenter), true);
public static readonly StyledProperty<bool> AutoCenterProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
nameof(AutoCenter),
true
);
/// <summary>
/// Gets or sets if image is auto centered
@ -993,10 +978,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly StyledProperty<bool> AllowZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(AllowZoom), true);
public static readonly StyledProperty<bool> AllowZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
nameof(AllowZoom),
true
);
/// <summary>
/// Gets or sets if zoom is allowed
@ -1007,14 +992,12 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(AllowZoomProperty, value);
}
public static readonly DirectProperty<
AdvancedImageBox,
ZoomLevelCollection
> ZoomLevelsProperty = AvaloniaProperty.RegisterDirect<AdvancedImageBox, ZoomLevelCollection>(
nameof(ZoomLevels),
o => o.ZoomLevels,
(o, v) => o.ZoomLevels = v
);
public static readonly DirectProperty<AdvancedImageBox, ZoomLevelCollection> ZoomLevelsProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, ZoomLevelCollection>(
nameof(ZoomLevels),
o => o.ZoomLevels,
(o, v) => o.ZoomLevels = v
);
ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default;
@ -1028,10 +1011,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value);
}
public static readonly StyledProperty<int> MinZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
int
>(nameof(MinZoom), 10);
public static readonly StyledProperty<int> MinZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
nameof(MinZoom),
10
);
/// <summary>
/// Gets or sets the minimum possible zoom.
@ -1043,10 +1026,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(MinZoomProperty, value);
}
public static readonly StyledProperty<int> MaxZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
int
>(nameof(MaxZoom), 6400);
public static readonly StyledProperty<int> MaxZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
nameof(MaxZoom),
6400
);
/// <summary>
/// Gets or sets the maximum possible zoom.
@ -1058,8 +1041,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(MaxZoomProperty, value);
}
public static readonly StyledProperty<bool> ConstrainZoomOutToFitLevelProperty =
AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(ConstrainZoomOutToFitLevel), true);
public static readonly StyledProperty<bool> ConstrainZoomOutToFitLevelProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(ConstrainZoomOutToFitLevel), true);
/// <summary>
/// Gets or sets if the zoom out should constrain to fit image as the lowest zoom level.
@ -1070,8 +1055,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(ConstrainZoomOutToFitLevelProperty, value);
}
public static readonly DirectProperty<AdvancedImageBox, int> OldZoomProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, int>(nameof(OldZoom), o => o.OldZoom);
public static readonly DirectProperty<AdvancedImageBox, int> OldZoomProperty = AvaloniaProperty.RegisterDirect<
AdvancedImageBox,
int
>(nameof(OldZoom), o => o.OldZoom);
private int _oldZoom = 100;
@ -1085,10 +1072,10 @@ public class AdvancedImageBox : TemplatedControl
private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value);
}
public static readonly StyledProperty<int> ZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
int
>(nameof(Zoom), 100);
public static readonly StyledProperty<int> ZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
nameof(Zoom),
100
);
/// <summary>
/// Gets or sets the zoom.
@ -1178,11 +1165,10 @@ public class AdvancedImageBox : TemplatedControl
/// <value>The height of the scaled image.</value>
public double ScaledImageHeight => Image?.Size.Height * ZoomFactor ?? 0;
public static readonly StyledProperty<ISolidColorBrush> PixelGridColorProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(PixelGridColor),
Brushes.DimGray
);
public static readonly StyledProperty<ISolidColorBrush> PixelGridColorProperty = AvaloniaProperty.Register<
AdvancedImageBox,
ISolidColorBrush
>(nameof(PixelGridColor), Brushes.DimGray);
/// <summary>
/// Gets or sets the color of the pixel grid.
@ -1194,8 +1180,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PixelGridColorProperty, value);
}
public static readonly StyledProperty<int> PixelGridZoomThresholdProperty =
AvaloniaProperty.Register<AdvancedImageBox, int>(nameof(PixelGridZoomThreshold), 13);
public static readonly StyledProperty<int> PixelGridZoomThresholdProperty = AvaloniaProperty.Register<
AdvancedImageBox,
int
>(nameof(PixelGridZoomThreshold), 13);
/// <summary>
/// Minimum size of zoomed pixel's before the pixel grid will be drawn
@ -1206,11 +1194,15 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PixelGridZoomThresholdProperty, value);
}
public static readonly StyledProperty<SelectionModes> SelectionModeProperty =
AvaloniaProperty.Register<AdvancedImageBox, SelectionModes>(nameof(SelectionMode));
public static readonly StyledProperty<SelectionModes> SelectionModeProperty = AvaloniaProperty.Register<
AdvancedImageBox,
SelectionModes
>(nameof(SelectionMode));
public static readonly StyledProperty<bool> IsPixelGridEnabledProperty =
AvaloniaProperty.Register<AdvancedImageBox, bool>("IsPixelGridEnabled", true);
public static readonly StyledProperty<bool> IsPixelGridEnabledProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>("IsPixelGridEnabled", true);
/// <summary>
/// Whether or not to draw the pixel grid at the <see cref="PixelGridZoomThreshold"/>
@ -1227,11 +1219,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(SelectionModeProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> SelectionColorProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(SelectionColor),
new SolidColorBrush(new Color(127, 0, 128, 255))
);
public static readonly StyledProperty<ISolidColorBrush> SelectionColorProperty = AvaloniaProperty.Register<
AdvancedImageBox,
ISolidColorBrush
>(nameof(SelectionColor), new SolidColorBrush(new Color(127, 0, 128, 255)));
public ISolidColorBrush SelectionColor
{
@ -1313,14 +1304,9 @@ public class AdvancedImageBox : TemplatedControl
AffectsRender<AdvancedImageBox>(ShowGridProperty);
HorizontalScrollBar =
e.NameScope.Find<ScrollBar>("PART_HorizontalScrollBar")
?? throw new NullReferenceException();
VerticalScrollBar =
e.NameScope.Find<ScrollBar>("PART_VerticalScrollBar")
?? throw new NullReferenceException();
ViewPort =
e.NameScope.Find<ContentPresenter>("PART_ViewPort")
?? throw new NullReferenceException();
e.NameScope.Find<ScrollBar>("PART_HorizontalScrollBar") ?? throw new NullReferenceException();
VerticalScrollBar = e.NameScope.Find<ScrollBar>("PART_VerticalScrollBar") ?? throw new NullReferenceException();
ViewPort = e.NameScope.Find<ContentPresenter>("PART_ViewPort") ?? throw new NullReferenceException();
SizeModeChanged();
@ -1347,13 +1333,12 @@ public class AdvancedImageBox : TemplatedControl
// If we're in high zoom, switch off bitmap interpolation mode
// Otherwise use high quality
BitmapInterpolationMode = isHighZoom
? BitmapInterpolationMode.None
: BitmapInterpolationMode.HighQuality;
BitmapInterpolationMode = isHighZoom ? BitmapInterpolationMode.None : BitmapInterpolationMode.HighQuality;
InvalidateVisual();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RenderBackgroundGrid(DrawingContext context)
{
var size = GridCellSize;
@ -1387,8 +1372,6 @@ public class AdvancedImageBox : TemplatedControl
public override void Render(DrawingContext context)
{
base.Render(context);
var gridCellSize = GridCellSize;
if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible))
@ -1399,9 +1382,7 @@ public class AdvancedImageBox : TemplatedControl
var zoomFactor = ZoomFactor;
var shouldDrawPixelGrid =
IsPixelGridEnabled
&& SizeMode == SizeModes.Normal
&& zoomFactor > PixelGridZoomThreshold;
IsPixelGridEnabled && SizeMode == SizeModes.Normal && zoomFactor > PixelGridZoomThreshold;
// Draw Grid
/*var viewPortSize = ViewPortSize;
@ -1441,16 +1422,10 @@ public class AdvancedImageBox : TemplatedControl
if (HaveTrackerImage && _pointerPosition is { X: >= 0, Y: >= 0 })
{
var destSize = TrackerImageAutoZoom
? new Size(
_trackerImage!.Size.Width * zoomFactor,
_trackerImage.Size.Height * zoomFactor
)
? new Size(_trackerImage!.Size.Width * zoomFactor, _trackerImage.Size.Height * zoomFactor)
: image.Size;
var destPos = new Point(
_pointerPosition.X - destSize.Width / 2,
_pointerPosition.Y - destSize.Height / 2
);
var destPos = new Point(_pointerPosition.X - destSize.Width / 2, _pointerPosition.Y - destSize.Height / 2);
context.DrawImage(_trackerImage!, new Rect(destPos, destSize));
}
@ -1462,30 +1437,14 @@ public class AdvancedImageBox : TemplatedControl
var offsetY = Offset.Y % zoomFactor;
Pen pen = new(PixelGridColor);
for (
var x = imageViewPort.X + zoomFactor - offsetX;
x < imageViewPort.Right;
x += zoomFactor
)
for (var x = imageViewPort.X + zoomFactor - offsetX; x < imageViewPort.Right; x += zoomFactor)
{
context.DrawLine(
pen,
new Point(x, imageViewPort.X),
new Point(x, imageViewPort.Bottom)
);
context.DrawLine(pen, new Point(x, imageViewPort.X), new Point(x, imageViewPort.Bottom));
}
for (
var y = imageViewPort.Y + zoomFactor - offsetY;
y < imageViewPort.Bottom;
y += zoomFactor
)
for (var y = imageViewPort.Y + zoomFactor - offsetY; y < imageViewPort.Bottom; y += zoomFactor)
{
context.DrawLine(
pen,
new Point(imageViewPort.Y, y),
new Point(imageViewPort.Right, y)
);
context.DrawLine(pen, new Point(imageViewPort.Y, y), new Point(imageViewPort.Right, y));
}
context.DrawRectangle(pen, imageViewPort);
@ -1496,12 +1455,7 @@ public class AdvancedImageBox : TemplatedControl
var rect = GetOffsetRectangle(SelectionRegion);
var selectionColor = SelectionColor;
context.FillRectangle(selectionColor, rect);
var color = Color.FromArgb(
255,
selectionColor.Color.R,
selectionColor.Color.G,
selectionColor.Color.B
);
var color = Color.FromArgb(255, selectionColor.Color.R, selectionColor.Color.G, selectionColor.Color.B);
context.DrawRectangle(new Pen(color.ToUInt32()), rect);
}
}
@ -1609,8 +1563,7 @@ public class AdvancedImageBox : TemplatedControl
{
if (
!(
pointer.Properties.IsLeftButtonPressed
&& (SelectWithMouseButtons & MouseButtons.LeftButton) != 0
pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0
|| pointer.Properties.IsMiddleButtonPressed
&& (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0
|| pointer.Properties.IsRightButtonPressed
@ -1624,12 +1577,10 @@ public class AdvancedImageBox : TemplatedControl
{
if (
!(
pointer.Properties.IsLeftButtonPressed
&& (PanWithMouseButtons & MouseButtons.LeftButton) != 0
pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0
|| pointer.Properties.IsMiddleButtonPressed
&& (PanWithMouseButtons & MouseButtons.MiddleButton) != 0
|| pointer.Properties.IsRightButtonPressed
&& (PanWithMouseButtons & MouseButtons.RightButton) != 0
|| pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0
)
|| !AutoPan
|| SizeMode != SizeModes.Normal
@ -2669,12 +2620,8 @@ public class AdvancedImageBox : TemplatedControl
case SizeModes.Normal:
if (AutoCenter)
{
xOffset = (
!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0
);
yOffset = (
!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0
);
xOffset = (!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0);
yOffset = (!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0);
}
width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), viewPortSize.Width);
@ -2729,12 +2676,7 @@ public class AdvancedImageBox : TemplatedControl
var pixelSize = SelectionPixelSize;
using var frameBuffer = image.Lock();
var newBitmap = new WriteableBitmap(
pixelSize,
image.Dpi,
frameBuffer.Format,
AlphaFormat.Unpremul
);
var newBitmap = new WriteableBitmap(pixelSize, image.Dpi, frameBuffer.Format, AlphaFormat.Unpremul);
using var newFrameBuffer = newBitmap.Lock();
var i = 0;

85
StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs

@ -15,24 +15,28 @@ public class BetterAdvancedImage : AdvancedImage
#region Reflection Shenanigans to access private parent fields
[NotNull]
private static readonly FieldInfo? IsCornerRadiusUsedField = typeof(AdvancedImage).GetField(
"_isCornerRadiusUsed",BindingFlags.Instance | BindingFlags.NonPublic);
"_isCornerRadiusUsed",
BindingFlags.Instance | BindingFlags.NonPublic
);
[NotNull]
private static readonly FieldInfo? CornerRadiusClipField = typeof(AdvancedImage).GetField(
"_cornerRadiusClip",BindingFlags.Instance | BindingFlags.NonPublic);
"_cornerRadiusClip",
BindingFlags.Instance | BindingFlags.NonPublic
);
private bool IsCornerRadiusUsed
{
get => IsCornerRadiusUsedField.GetValue(this) as bool? ?? false;
set => IsCornerRadiusUsedField.SetValue(this, value);
}
private RoundedRect CornerRadiusClip
{
get => (RoundedRect) CornerRadiusClipField.GetValue(this)!;
get => (RoundedRect)CornerRadiusClipField.GetValue(this)!;
set => CornerRadiusClipField.SetValue(this, value);
}
static BetterAdvancedImage()
{
if (IsCornerRadiusUsedField is null)
@ -45,16 +49,14 @@ public class BetterAdvancedImage : AdvancedImage
}
}
#endregion
protected override Type StyleKeyOverride { get; } = typeof(AdvancedImage);
public BetterAdvancedImage(Uri? baseUri) : base(baseUri)
{
}
public BetterAdvancedImage(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
public BetterAdvancedImage(Uri? baseUri)
: base(baseUri) { }
public BetterAdvancedImage(IServiceProvider serviceProvider)
: base(serviceProvider) { }
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <inheritdoc />
@ -69,61 +71,66 @@ public class BetterAdvancedImage : AdvancedImage
var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
var scaledSize = sourceSize * scale;
// Calculate starting points for dest
var destX = HorizontalContentAlignment switch
{
HorizontalAlignment.Left => 0,
HorizontalAlignment.Center => (int) (viewPort.Width - scaledSize.Width) / 2,
HorizontalAlignment.Right => (int) (viewPort.Width - scaledSize.Width),
HorizontalAlignment.Center => (int)(viewPort.Width - scaledSize.Width) / 2,
HorizontalAlignment.Right => (int)(viewPort.Width - scaledSize.Width),
// Stretch is default, use center
HorizontalAlignment.Stretch => (int) (viewPort.Width - scaledSize.Width) / 2,
HorizontalAlignment.Stretch
=> (int)(viewPort.Width - scaledSize.Width) / 2,
_ => throw new ArgumentException(nameof(HorizontalContentAlignment))
};
var destY = VerticalContentAlignment switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Center => (int) (viewPort.Height - scaledSize.Height) / 2,
VerticalAlignment.Bottom => (int) (viewPort.Height - scaledSize.Height),
VerticalAlignment.Center => (int)(viewPort.Height - scaledSize.Height) / 2,
VerticalAlignment.Bottom => (int)(viewPort.Height - scaledSize.Height),
VerticalAlignment.Stretch => 0, // Stretch is default, use top
_ => throw new ArgumentException(nameof(VerticalContentAlignment))
};
var destRect = viewPort
.CenterRect(new Rect(scaledSize))
.WithX(destX)
.WithY(destY)
.Intersect(viewPort);
var destRect = viewPort.CenterRect(new Rect(scaledSize)).WithX(destX).WithY(destY).Intersect(viewPort);
var destRectUnscaledSize = destRect.Size / scale;
// Calculate starting points for source
var sourceX = HorizontalContentAlignment switch
{
HorizontalAlignment.Left => 0,
HorizontalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Width / 2,
HorizontalAlignment.Right => (int) (sourceSize - destRectUnscaledSize).Width,
HorizontalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Width / 2,
HorizontalAlignment.Right => (int)(sourceSize - destRectUnscaledSize).Width,
// Stretch is default, use center
HorizontalAlignment.Stretch => (int) (sourceSize - destRectUnscaledSize).Width / 2,
HorizontalAlignment.Stretch
=> (int)(sourceSize - destRectUnscaledSize).Width / 2,
_ => throw new ArgumentException(nameof(HorizontalContentAlignment))
};
var sourceY = VerticalContentAlignment switch
{
VerticalAlignment.Top => 0,
VerticalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Height / 2,
VerticalAlignment.Bottom => (int) (sourceSize - destRectUnscaledSize).Height,
VerticalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Height / 2,
VerticalAlignment.Bottom => (int)(sourceSize - destRectUnscaledSize).Height,
VerticalAlignment.Stretch => 0, // Stretch is default, use top
_ => throw new ArgumentException(nameof(VerticalContentAlignment))
};
var sourceRect = new Rect(sourceSize)
.CenterRect(new Rect(destRect.Size / scale))
.WithX(sourceX)
.WithY(sourceY);
DrawingContext.PushedState? pushedState =
IsCornerRadiusUsed ? context.PushClip(CornerRadiusClip) : null;
context.DrawImage(source, sourceRect, destRect);
pushedState?.Dispose();
if (IsCornerRadiusUsed)
{
using (context.PushClip(CornerRadiusClip))
{
context.DrawImage(source, sourceRect, destRect);
}
}
else
{
context.DrawImage(source, sourceRect, destRect);
}
}
else
{

113
StabilityMatrix.Avalonia/Controls/Extensions/ShowDisabledTooltipExtension.cs

@ -0,0 +1,113 @@
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace StabilityMatrix.Avalonia.Controls.Extensions;
/// <summary>
/// Show tooltip on Controls with IsEffectivelyEnabled = false
/// https://github.com/AvaloniaUI/Avalonia/issues/3847#issuecomment-1618790059
/// </summary>
[PublicAPI]
public static class ShowDisabledTooltipExtension
{
static ShowDisabledTooltipExtension()
{
ShowOnDisabledProperty.Changed.AddClassHandler<Control>(HandleShowOnDisabledChanged);
}
public static bool GetShowOnDisabled(AvaloniaObject obj)
{
return obj.GetValue(ShowOnDisabledProperty);
}
public static void SetShowOnDisabled(AvaloniaObject obj, bool value)
{
obj.SetValue(ShowOnDisabledProperty, value);
}
public static readonly AttachedProperty<bool> ShowOnDisabledProperty = AvaloniaProperty.RegisterAttached<
object,
Control,
bool
>("ShowOnDisabled");
private static void HandleShowOnDisabledChanged(Control control, AvaloniaPropertyChangedEventArgs e)
{
if (e.GetNewValue<bool>())
{
control.DetachedFromVisualTree += AttachedControl_DetachedFromVisualOrExtension;
control.AttachedToVisualTree += AttachedControl_AttachedToVisualTree;
if (control.IsInitialized)
{
// enabled after visual attached
AttachedControl_AttachedToVisualTree(control, null!);
}
}
else
{
AttachedControl_DetachedFromVisualOrExtension(control, null!);
}
}
private static void AttachedControl_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (sender is not Control control || TopLevel.GetTopLevel(control) is not { } tl)
{
return;
}
// NOTE pointermove needed to be tunneled for me but you may not need to...
tl.AddHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved, RoutingStrategies.Tunnel);
}
private static void AttachedControl_DetachedFromVisualOrExtension(object? s, VisualTreeAttachmentEventArgs e)
{
if (s is not Control control)
{
return;
}
control.DetachedFromVisualTree -= AttachedControl_DetachedFromVisualOrExtension;
control.AttachedToVisualTree -= AttachedControl_AttachedToVisualTree;
if (TopLevel.GetTopLevel(control) is not { } tl)
{
return;
}
tl.RemoveHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved);
}
private static void TopLevel_PointerMoved(object? sender, PointerEventArgs e)
{
if (sender is not Control tl)
{
return;
}
var attachedControls = tl.GetVisualDescendants().Where(GetShowOnDisabled).Cast<Control>().ToList();
// find disabled children under pointer w/ this extension enabled
var disabledChildUnderPointer = attachedControls.FirstOrDefault(
x =>
x.Bounds.Contains(e.GetPosition(x.Parent as Visual))
&& x is { IsEffectivelyVisible: true, IsEffectivelyEnabled: false }
);
if (disabledChildUnderPointer != null)
{
// manually show tooltip
ToolTip.SetIsOpen(disabledChildUnderPointer, true);
}
var disabledTooltipsToHide = attachedControls.Where(
x => ToolTip.GetIsOpen(x) && x != disabledChildUnderPointer && !x.IsEffectivelyEnabled
);
foreach (var control in disabledTooltipsToHide)
{
ToolTip.SetIsOpen(control, false);
}
}
}

37
StabilityMatrix.Avalonia/Controls/LineDashFrame.cs

@ -12,8 +12,10 @@ public class LineDashFrame : Frame
{
protected override Type StyleKeyOverride { get; } = typeof(Frame);
public static readonly StyledProperty<ISolidColorBrush> StrokeProperty =
AvaloniaProperty.Register<LineDashFrame, ISolidColorBrush>("Stroke");
public static readonly StyledProperty<ISolidColorBrush> StrokeProperty = AvaloniaProperty.Register<
LineDashFrame,
ISolidColorBrush
>("Stroke");
public ISolidColorBrush Stroke
{
@ -21,8 +23,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeProperty, value);
}
public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<LineDashFrame, double>("StrokeThickness");
public static readonly StyledProperty<double> StrokeThicknessProperty = AvaloniaProperty.Register<
LineDashFrame,
double
>("StrokeThickness");
public double StrokeThickness
{
@ -30,8 +34,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeThicknessProperty, value);
}
public static readonly StyledProperty<double> StrokeDashLineProperty =
AvaloniaProperty.Register<LineDashFrame, double>("StrokeDashLine");
public static readonly StyledProperty<double> StrokeDashLineProperty = AvaloniaProperty.Register<
LineDashFrame,
double
>("StrokeDashLine");
public double StrokeDashLine
{
@ -39,8 +45,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeDashLineProperty, value);
}
public static readonly StyledProperty<double> StrokeDashSpaceProperty =
AvaloniaProperty.Register<LineDashFrame, double>("StrokeDashSpace");
public static readonly StyledProperty<double> StrokeDashSpaceProperty = AvaloniaProperty.Register<
LineDashFrame,
double
>("StrokeDashSpace");
public double StrokeDashSpace
{
@ -48,8 +56,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeDashSpaceProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> FillProperty =
AvaloniaProperty.Register<LineDashFrame, ISolidColorBrush>("Fill");
public static readonly StyledProperty<ISolidColorBrush> FillProperty = AvaloniaProperty.Register<
LineDashFrame,
ISolidColorBrush
>("Fill");
public ISolidColorBrush Fill
{
@ -82,17 +92,12 @@ public class LineDashFrame : Frame
/// <inheritdoc />
public override void Render(DrawingContext context)
{
base.Render(context);
var width = Bounds.Width;
var height = Bounds.Height;
context.DrawRectangle(Fill, null, new Rect(0, 0, width, height));
var dashPen = new Pen(Stroke, StrokeThickness)
{
DashStyle = new DashStyle(GetDashArray(width), 0)
};
var dashPen = new Pen(Stroke, StrokeThickness) { DashStyle = new DashStyle(GetDashArray(width), 0) };
context.DrawLine(dashPen, new Point(0, 0), new Point(width, 0));
context.DrawLine(dashPen, new Point(0, height), new Point(width, height));

129
StabilityMatrix.Avalonia/Controls/ProgressRing.cs

@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
namespace StabilityMatrix.Avalonia.Controls;
@ -13,34 +15,11 @@ namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class ProgressRing : RangeBase
{
public static readonly StyledProperty<bool> IsIndeterminateProperty =
ProgressBar.IsIndeterminateProperty.AddOwner<ProgressRing>();
private Arc? fillArc;
public static readonly StyledProperty<bool> PreserveAspectProperty =
AvaloniaProperty.Register<ProgressRing, bool>(nameof(PreserveAspect), true);
public static readonly StyledProperty<double> ValueAngleProperty =
AvaloniaProperty.Register<ProgressRing, double>(nameof(ValueAngle));
public static readonly StyledProperty<double> StartAngleProperty =
AvaloniaProperty.Register<ProgressRing, double>(nameof(StartAngle));
public static readonly StyledProperty<double> EndAngleProperty =
AvaloniaProperty.Register<ProgressRing, double>(nameof(EndAngle), 360);
static ProgressRing()
{
MinimumProperty.Changed.AddClassHandler<ProgressRing>(OnMinimumPropertyChanged);
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnMaximumPropertyChanged);
ValueProperty.Changed.AddClassHandler<ProgressRing>(OnValuePropertyChanged);
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnStartAnglePropertyChanged);
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnEndAnglePropertyChanged);
}
public ProgressRing()
{
UpdatePseudoClasses(IsIndeterminate, PreserveAspect);
}
public static readonly StyledProperty<bool> IsIndeterminateProperty = ProgressBar
.IsIndeterminateProperty
.AddOwner<ProgressRing>();
public bool IsIndeterminate
{
@ -48,35 +27,90 @@ public class ProgressRing : RangeBase
set => SetValue(IsIndeterminateProperty, value);
}
public static readonly StyledProperty<bool> PreserveAspectProperty = AvaloniaProperty.Register<ProgressRing, bool>(
nameof(PreserveAspect),
true
);
public bool PreserveAspect
{
get => GetValue(PreserveAspectProperty);
set => SetValue(PreserveAspectProperty, value);
}
public double ValueAngle
public static readonly StyledProperty<double> StrokeThicknessProperty = Shape
.StrokeThicknessProperty
.AddOwner<ProgressRing>();
public double StrokeThickness
{
get => GetValue(ValueAngleProperty);
private set => SetValue(ValueAngleProperty, value);
get => GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
public static readonly StyledProperty<double> StartAngleProperty = AvaloniaProperty.Register<ProgressRing, double>(
nameof(StartAngle)
);
public double StartAngle
{
get => GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
}
public static readonly StyledProperty<double> SweepAngleProperty = AvaloniaProperty.Register<ProgressRing, double>(
nameof(SweepAngle)
);
public double SweepAngle
{
get => GetValue(SweepAngleProperty);
set => SetValue(SweepAngleProperty, value);
}
public static readonly StyledProperty<double> EndAngleProperty = AvaloniaProperty.Register<ProgressRing, double>(
nameof(EndAngle),
360
);
public double EndAngle
{
get => GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
}
static ProgressRing()
{
AffectsRender<ProgressRing>(SweepAngleProperty, StartAngleProperty, EndAngleProperty);
ValueProperty.Changed.AddClassHandler<ProgressRing>(OnValuePropertyChanged);
SweepAngleProperty.Changed.AddClassHandler<ProgressRing>(OnSweepAnglePropertyChanged);
}
public ProgressRing()
{
UpdatePseudoClasses(IsIndeterminate, PreserveAspect);
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
fillArc = e.NameScope.Find<Arc>("PART_Fill");
if (fillArc is not null)
{
fillArc.StartAngle = StartAngle;
fillArc.SweepAngle = SweepAngle;
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
var e = change as AvaloniaPropertyChangedEventArgs<bool>;
if (e is null) return;
if (e is null)
return;
if (e.Property == IsIndeterminateProperty)
{
@ -88,9 +122,7 @@ public class ProgressRing : RangeBase
}
}
private void UpdatePseudoClasses(
bool? isIndeterminate,
bool? preserveAspect)
private void UpdatePseudoClasses(bool? isIndeterminate, bool? preserveAspect)
{
if (isIndeterminate.HasValue)
{
@ -103,28 +135,19 @@ public class ProgressRing : RangeBase
}
}
private static void OnMinimumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
sender.Minimum = (double) e.NewValue!;
}
private static void OnMaximumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
sender.Maximum = (double) e.NewValue!;
}
private static void OnValuePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
sender.ValueAngle = ((double) e.NewValue! - sender.Minimum) * (sender.EndAngle - sender.StartAngle) / (sender.Maximum - sender.Minimum);
}
private static void OnStartAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
sender.StartAngle = (double) e.NewValue!;
sender.SweepAngle =
((double)e.NewValue! - sender.Minimum)
* (sender.EndAngle - sender.StartAngle)
/ (sender.Maximum - sender.Minimum);
}
private static void OnEndAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
private static void OnSweepAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
sender.EndAngle = (double) e.NewValue!;
if (sender.fillArc is { } arc)
{
arc.SweepAngle = Math.Round(e.GetNewValue<double>());
}
}
}

124
StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.PropertyGrid.Services;
using JetBrains.Annotations;
using PropertyModels.ComponentModel;
using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Avalonia.Controls;
/// <inheritdoc />
[PublicAPI]
public class BetterPropertyGrid : global::Avalonia.PropertyGrid.Controls.PropertyGrid
{
protected override Type StyleKeyOverride => typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid);
public static readonly StyledProperty<IEnumerable<string>> ExcludedCategoriesProperty = AvaloniaProperty.Register<
BetterPropertyGrid,
IEnumerable<string>
>("ExcludedCategories");
public IEnumerable<string> ExcludedCategories
{
get => GetValue(ExcludedCategoriesProperty);
set => SetValue(ExcludedCategoriesProperty, value);
}
public static readonly StyledProperty<IEnumerable<string>> IncludedCategoriesProperty = AvaloniaProperty.Register<
BetterPropertyGrid,
IEnumerable<string>
>("IncludedCategories");
public IEnumerable<string> IncludedCategories
{
get => GetValue(IncludedCategoriesProperty);
set => SetValue(IncludedCategoriesProperty, value);
}
static BetterPropertyGrid()
{
// Register factories
CellEditFactoryService.Default.AddFactory(new ToggleSwitchCellEditFactory());
// Initialize localization and name resolver
LocalizationService.Default.AddExtraService(new PropertyGridLocalizationService());
ExcludedCategoriesProperty
.Changed
.AddClassHandler<BetterPropertyGrid>(
(grid, args) =>
{
if (args.NewValue is IEnumerable<string> excludedCategories)
{
grid.FilterExcludeCategories(excludedCategories);
}
}
);
IncludedCategoriesProperty
.Changed
.AddClassHandler<BetterPropertyGrid>(
(grid, args) =>
{
if (args.NewValue is IEnumerable<string> includedCategories)
{
grid.FilterIncludeCategories(includedCategories);
}
}
);
}
public void FilterExcludeCategories(IEnumerable<string> excludedCategories)
{
// Get internal property `ViewModel` of internal type `PropertyGridViewModel`
var gridVm = this.GetProtectedProperty("ViewModel")!;
// Get public property `CategoryFilter`
var categoryFilter = gridVm.GetProtectedProperty<CheckedMaskModel>("CategoryFilter")!;
categoryFilter.BeginUpdate();
// Uncheck All, then check all except All
categoryFilter.UnCheck(categoryFilter.All);
foreach (var mask in categoryFilter.Masks.Where(m => m != categoryFilter.All))
{
categoryFilter.Check(mask);
}
// Uncheck excluded categories
foreach (var mask in excludedCategories)
{
categoryFilter.UnCheck(mask);
}
categoryFilter.EndUpdate();
}
public void FilterIncludeCategories(IEnumerable<string> includeCategories)
{
// Get internal property `ViewModel` of internal type `PropertyGridViewModel`
var gridVm = this.GetProtectedProperty("ViewModel")!;
// Get public property `CategoryFilter`
var categoryFilter = gridVm.GetProtectedProperty<CheckedMaskModel>("CategoryFilter")!;
categoryFilter.BeginUpdate();
// Uncheck non-included categories
foreach (var mask in categoryFilter.Masks.Where(m => !includeCategories.Contains(m)))
{
categoryFilter.UnCheck(mask);
}
categoryFilter.UnCheck(categoryFilter.All);
// Check included categories
foreach (var mask in includeCategories)
{
categoryFilter.Check(mask);
}
categoryFilter.EndUpdate();
}
}

35
StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs

@ -0,0 +1,35 @@
using System;
using System.Globalization;
using PropertyModels.Localilzation;
using StabilityMatrix.Avalonia.Languages;
namespace StabilityMatrix.Avalonia.Controls;
internal class PropertyGridCultureData : ICultureData
{
/// <inheritdoc />
public bool Reload() => false;
/// <inheritdoc />
public CultureInfo Culture => Cultures.Current ?? Cultures.Default;
/// <inheritdoc />
public Uri Path => new("");
/// <inheritdoc />
public string this[string key]
{
get
{
if (Resources.ResourceManager.GetString(key) is { } result)
{
return result;
}
return key;
}
}
/// <inheritdoc />
public bool IsLoaded => true;
}

36
StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs

@ -0,0 +1,36 @@
using System;
using PropertyModels.ComponentModel;
using PropertyModels.Localilzation;
using StabilityMatrix.Avalonia.Languages;
namespace StabilityMatrix.Avalonia.Controls;
/// <summary>
/// Implements <see cref="ILocalizationService"/> using static <see cref="Cultures"/>.
/// </summary>
internal class PropertyGridLocalizationService : MiniReactiveObject, ILocalizationService
{
/// <inheritdoc />
public ICultureData CultureData { get; } = new PropertyGridCultureData();
/// <inheritdoc />
public string this[string key] => CultureData[key];
/// <inheritdoc />
public event EventHandler? OnCultureChanged;
/// <inheritdoc />
public ILocalizationService[] GetExtraServices() => Array.Empty<ILocalizationService>();
/// <inheritdoc />
public void AddExtraService(ILocalizationService service) { }
/// <inheritdoc />
public void RemoveExtraService(ILocalizationService service) { }
/// <inheritdoc />
public ICultureData[] GetCultures() => new[] { CultureData };
/// <inheritdoc />
public void SelectCulture(string cultureName) { }
}

60
StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs

@ -0,0 +1,60 @@
using Avalonia.Controls;
using Avalonia.PropertyGrid.Controls;
using Avalonia.PropertyGrid.Controls.Factories;
using Avalonia.PropertyGrid.Localization;
namespace StabilityMatrix.Avalonia.Controls;
internal class ToggleSwitchCellEditFactory : AbstractCellEditFactory
{
// make this extend factor only effect on TestExtendPropertyGrid
public override bool Accept(object accessToken)
{
return accessToken is BetterPropertyGrid;
}
public override Control? HandleNewProperty(PropertyCellContext context)
{
var propertyDescriptor = context.Property;
var target = context.Target;
if (propertyDescriptor.PropertyType != typeof(bool))
{
return null;
}
var control = new ToggleSwitch();
control.SetLocalizeBinding(ToggleSwitch.OnContentProperty, "On");
control.SetLocalizeBinding(ToggleSwitch.OffContentProperty, "Off");
control.IsCheckedChanged += (s, e) =>
{
SetAndRaise(context, control, control.IsChecked);
};
return control;
}
public override bool HandlePropertyChanged(PropertyCellContext context)
{
var propertyDescriptor = context.Property;
var target = context.Target;
var control = context.CellEdit;
if (propertyDescriptor.PropertyType != typeof(bool))
{
return false;
}
ValidateProperty(control, propertyDescriptor, target);
if (control is ToggleSwitch ts)
{
ts.IsChecked = (bool)(propertyDescriptor.GetValue(target) ?? false);
return true;
}
return false;
}
}

14
StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml

@ -62,19 +62,7 @@
<ListBox.DataTemplates>
<local:ViewLocator/>
</ListBox.DataTemplates>
<!--<ListBox.Styles>
<Style Selector="ListBoxItem:not(:dragging)">
<Setter Property="Transitions">
<Setter.Value>
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.1" />
</Transitions>
</Setter.Value>
</Setter>
</Style>
</ListBox.Styles>-->
</ListBox>
</Grid>

104
StabilityMatrix.Avalonia/Controls/StackExpander.axaml

@ -1,85 +1,99 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
x:DataType="vmInference:StackExpanderViewModel">
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
x:DataType="vmInference:StackExpanderViewModel">
<Design.PreviewWith>
<Grid Width="500" Height="800">
<StackPanel>
<controls:StackExpander
DataContext="{x:Static mocks:DesignData.StackExpanderViewModel}"/>
<controls:StackExpander DataContext="{x:Static mocks:DesignData.StackExpanderViewModel}" />
<controls:StackExpander DataContext="{x:Static mocks:DesignData.StackExpanderViewModel2}" />
</StackPanel>
</Grid>
</Design.PreviewWith>
<Style Selector="controls|StackExpander">
<!-- Set Defaults -->
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<!-- Set Defaults -->
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<Expander
VerticalContentAlignment="Top"
ExpandDirection="{TemplateBinding ExpandDirection}"
IsExpanded="{TemplateBinding IsExpanded}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
HorizontalContentAlignment="Stretch"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
VerticalContentAlignment="Top">
<Expander.Styles>
<Style Selector="Expander /template/ ToggleButton#PART_toggle">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</Expander.Styles>
<Expander.Header>
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<ToggleSwitch
Margin="2,0,0,2"
IsChecked="{Binding IsEnabled}"
VerticalContentAlignment="Center"
OnContent=""
OffContent=""/>
IsChecked="{Binding IsEnabled}"
OffContent=""
OnContent="" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding TitleExtra}"
IsVisible="{Binding TitleExtra, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock
IsVisible="{Binding TitleExtra, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Text="{Binding TitleExtra}" />
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Text="{Binding Title}"/>
<Button
IsVisible="{Binding IsEditEnabled}"
Command="{Binding RemoveFromParentListCommand}"
Text="{Binding Title}" />
<!-- Settings button -->
<Button
Grid.Column="3"
Padding="12,6"
HorizontalAlignment="Right"
Classes="transparent-full"
Command="{Binding SettingsCommand}"
IsVisible="{Binding IsSettingsEnabled}">
<fluentIcons:SymbolIcon FontSize="17" Symbol="Settings" />
</Button>
<!-- Delete button for StackEditableCard -->
<Button
Grid.Column="4"
Padding="12,6"
HorizontalAlignment="Right"
Classes="transparent-red"
Grid.Column="3"
Padding="10,4"
HorizontalAlignment="Right">
<fluentIcons:SymbolIcon Symbol="Delete" FontSize="16" />
Command="{Binding RemoveFromParentListCommand}"
IsVisible="{Binding IsEditEnabled}">
<fluentIcons:SymbolIcon FontSize="16" Symbol="Delete" />
</Button>
</Grid>
</Expander.Header>
<Panel>
<ItemsControl
VerticalAlignment="Top"
ItemsSource="{Binding Cards}">
<ItemsControl VerticalAlignment="Top" ItemsSource="{Binding Cards}">
<ItemsControl.Styles>
<Style Selector="controls|Card">
<Setter Property="IsCardVisualsEnabled" Value="False"/>
<Setter Property="IsCardVisualsEnabled" Value="False" />
</Style>
</ItemsControl.Styles>
<ItemsControl.DataTemplates>
<local:ViewLocator/>
<local:ViewLocator />
</ItemsControl.DataTemplates>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
<StackPanel
x:Name="PART_ItemsPanel"
VerticalAlignment="Top"
Spacing="{TemplateBinding Spacing}" />
@ -88,7 +102,7 @@
</ItemsControl>
</Panel>
</Expander>
</ControlTemplate>
</Setter>

29
StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs

@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
@ -7,10 +8,30 @@ namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class StackExpander : TemplatedControl
{
public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register<
StackCard,
int
>("Spacing", 8);
public static readonly StyledProperty<bool> IsExpandedProperty = Expander
.IsExpandedProperty
.AddOwner<StackExpander>();
public static readonly StyledProperty<ExpandDirection> ExpandDirectionProperty = Expander
.ExpandDirectionProperty
.AddOwner<StackExpander>();
public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register<StackCard, int>(
"Spacing",
8
);
public ExpandDirection ExpandDirection
{
get => GetValue(ExpandDirectionProperty);
set => SetValue(ExpandDirectionProperty, value);
}
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
public int Spacing
{

22
StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs

@ -0,0 +1,22 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
public class CustomStringFormatConverter<T>([StringSyntax("CompositeFormat")] string format) : IValueConverter
where T : IFormatProvider, new()
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is null ? null : string.Format(new T(), format, value);
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is null ? null : throw new NotImplementedException();
}
}

31
StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs

@ -0,0 +1,31 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
/// <summary>
/// Converts an index to index + 1
/// </summary>
public class IndexPlusOneConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int i)
{
return i + 1;
}
return value;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int i)
{
return i - 1;
}
return value;
}
}

41
StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs

@ -0,0 +1,41 @@
using System;
using Size = StabilityMatrix.Core.Helper.Size;
namespace StabilityMatrix.Avalonia.Converters;
public class MemoryBytesFormatter : ICustomFormatter, IFormatProvider
{
/// <inheritdoc />
public object? GetFormat(Type? formatType)
{
return formatType == typeof(ICustomFormatter) ? this : null;
}
/// <inheritdoc />
public string Format(string? format, object? arg, IFormatProvider? formatProvider)
{
if (format == null || !format.Trim().StartsWith('M'))
{
if (arg is IFormattable formatArg)
{
return formatArg.ToString(format, formatProvider);
}
return arg?.ToString() ?? string.Empty;
}
var value = Convert.ToUInt64(arg);
var result = format.Trim().EndsWith("10", StringComparison.OrdinalIgnoreCase)
? Size.FormatBase10Bytes(value)
: Size.FormatBytes(value);
// Strip i if not Mi
if (!format.Trim().Contains('I', StringComparison.OrdinalIgnoreCase))
{
result = result.Replace("i", string.Empty, StringComparison.OrdinalIgnoreCase);
}
return result;
}
}

8
StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs

@ -1,4 +1,5 @@
using Avalonia.Data.Converters;
using System;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
@ -7,4 +8,9 @@ public static class StringFormatConverters
private static StringFormatValueConverter? _decimalConverter;
public static StringFormatValueConverter Decimal =>
_decimalConverter ??= new StringFormatValueConverter("{0:D}", null);
private static readonly Lazy<IValueConverter> MemoryBytesConverterLazy =
new(() => new CustomStringFormatConverter<MemoryBytesFormatter>("{0:M}"));
public static IValueConverter MemoryBytes => MemoryBytesConverterLazy.Value;
}

253
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
@ -21,10 +23,10 @@ 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.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
@ -42,6 +44,8 @@ using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel;
using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.HuggingFacePageViewModel;
namespace StabilityMatrix.Avalonia.DesignData;
@ -76,10 +80,7 @@ public static class DesignData
Id = activePackageId,
DisplayName = "My Installed Package",
PackageName = "stable-diffusion-webui",
Version = new InstalledPackageVersion
{
InstalledReleaseVersion = "v1.0.0"
},
Version = new InstalledPackageVersion { InstalledReleaseVersion = "v1.0.0" },
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
},
@ -91,8 +92,7 @@ public static class DesignData
Version = new InstalledPackageVersion
{
InstalledBranch = "master",
InstalledCommitSha =
"abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
@ -125,7 +125,8 @@ public static class DesignData
.AddSingleton<IInferenceClientManager, MockInferenceClientManager>()
.AddSingleton<ICompletionProvider, MockCompletionProvider>()
.AddSingleton<IModelIndexService, MockModelIndexService>()
.AddSingleton<IImageIndexService, MockImageIndexService>();
.AddSingleton<IImageIndexService, MockImageIndexService>()
.AddSingleton<IMetadataImportService, MetadataImportService>();
// Placeholder services that nobody should need during design time
services
@ -148,6 +149,7 @@ public static class DesignData
var modelFinder = Services.GetRequiredService<ModelFinder>();
var packageFactory = Services.GetRequiredService<IPackageFactory>();
var notificationService = Services.GetRequiredService<INotificationService>();
var modelImportService = Services.GetRequiredService<IMetadataImportService>();
LaunchOptionsViewModel = Services.GetRequiredService<LaunchOptionsViewModel>();
LaunchOptionsViewModel.Cards = new[]
@ -184,7 +186,7 @@ public static class DesignData
CheckpointsPageViewModel.CheckpointFoldersCache,
new CheckpointFolder[]
{
new(settingsManager, downloadService, modelFinder, notificationService)
new(settingsManager, downloadService, modelFinder, notificationService, modelImportService)
{
DirectoryPath = "Models/StableDiffusion",
DisplayedCheckpointFiles = new ObservableCollectionExtended<CheckpointFile>()
@ -211,14 +213,10 @@ public static class DesignData
TrainedWords = ["aurora", "lightning"]
}
},
new()
{
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
},
new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" },
},
},
new(settingsManager, downloadService, modelFinder, notificationService)
new(settingsManager, downloadService, modelFinder, notificationService, modelImportService)
{
Title = "Lora",
DirectoryPath = "Packages/Lora",
@ -299,56 +297,55 @@ public static class DesignData
);
}*/
CheckpointBrowserViewModel.ModelCards =
new ObservableCollection<CheckpointBrowserCardViewModel>
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
vm.CivitModel = new CivitModel
{
vm.CivitModel = new CivitModel
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
ModelVersions = [new() { Name = "v1.2.2-Inpainting" }],
Creator = new CivitCreator
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
ModelVersions = [
new() { Name = "v1.2.2-Inpainting" }
],
Creator = new CivitCreator
{
Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro",
Username = "creator-1"
}
};
}),
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro",
Username = "creator-1"
}
};
}),
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = new CivitModel
{
vm.CivitModel = new CivitModel
{
Name = "Another Model",
Description = "A mix of example",
Stats = new CivitModelStats { Rating = 5, RatingCount = 3500 },
ModelVersions = [
new()
Name = "Another Model",
Description = "A mix of example",
Stats = new CivitModelStats { Rating = 5, RatingCount = 3500 },
ModelVersions =
[
new()
{
Name = "v1.2.2-Inpainting",
Images = new List<CivitImage>
{
Name = "v1.2.2-Inpainting",
Images = new List<CivitImage>
new()
{
new()
{
Nsfw = "None",
Url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
}
Nsfw = "None",
Url =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
}
}
],
Creator = new CivitCreator
{
Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro",
Username = "creator-2"
}
}
};
})
};
],
Creator = new CivitCreator
{
Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro",
Username = "creator-2"
}
};
})
};
NewCheckpointsPageViewModel.AllCheckpoints = new ObservableCollection<CheckpointFile>
{
@ -376,33 +373,30 @@ public static class DesignData
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...")
ProgressManagerViewModel
.ProgressItems
.AddRange(
new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(
new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))
),
new MockDownloadProgressItemViewModel(
"Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe"
),
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
)
),
new MockDownloadProgressItemViewModel(
"Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe"
),
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
)
}
);
}
);
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.CurrentVersionText = "v2.0.0";
UpdateViewModel.NewVersionText = "v2.0.1";
UpdateViewModel.ReleaseNotes =
"## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature";
UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature";
isInitialized = true;
}
@ -419,14 +413,15 @@ public static class DesignData
public static ServiceManager<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>();
public static MainWindowViewModel MainWindowViewModel =>
Services.GetRequiredService<MainWindowViewModel>();
public static MainWindowViewModel MainWindowViewModel => Services.GetRequiredService<MainWindowViewModel>();
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
Services.GetRequiredService<FirstLaunchSetupViewModel>();
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService<LaunchPageViewModel>();
public static HuggingFacePageViewModel HuggingFacePageViewModel =>
Services.GetRequiredService<HuggingFacePageViewModel>();
public static OutputsPageViewModel OutputsPageViewModel
{
@ -457,10 +452,7 @@ public static class DesignData
vm.SetPackages(settings.Settings.InstalledPackages);
vm.SetUnknownPackages(
new InstalledPackage[]
{
UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"),
}
new InstalledPackage[] { UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), }
);
vm.PackageCards[0].IsUpdateAvailable = true;
@ -475,14 +467,12 @@ public static class DesignData
public static NewCheckpointsPageViewModel NewCheckpointsPageViewModel =>
Services.GetRequiredService<NewCheckpointsPageViewModel>();
public static SettingsViewModel SettingsViewModel =>
Services.GetRequiredService<SettingsViewModel>();
public static SettingsViewModel SettingsViewModel => Services.GetRequiredService<SettingsViewModel>();
public static InferenceSettingsViewModel InferenceSettingsViewModel =>
Services.GetRequiredService<InferenceSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel =>
Services.GetRequiredService<MainSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel => Services.GetRequiredService<MainSettingsViewModel>();
public static AccountSettingsViewModel AccountSettingsViewModel =>
Services.GetRequiredService<AccountSettingsViewModel>();
@ -502,7 +492,7 @@ public static class DesignData
HashBlake3 = "",
Signature = "",
};
vm.UpdateStatus = new UpdateStatusChangedEventArgs
{
LatestUpdate = update,
@ -518,6 +508,9 @@ public static class DesignData
}
}
public static CivitAiBrowserViewModel CivitAiBrowserViewModel =>
Services.GetRequiredService<CivitAiBrowserViewModel>();
public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService<CheckpointBrowserViewModel>();
@ -563,10 +556,7 @@ The gallery images are often inpainted, but you will get something very similar
}
}
};
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 };
@ -578,8 +568,7 @@ The gallery images are often inpainted, but you will get something very similar
public static OneClickInstallViewModel OneClickInstallViewModel =>
Services.GetRequiredService<OneClickInstallViewModel>();
public static InferenceViewModel InferenceViewModel =>
Services.GetRequiredService<InferenceViewModel>();
public static InferenceViewModel InferenceViewModel => Services.GetRequiredService<InferenceViewModel>();
public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel =>
Services.GetRequiredService<SelectDataDirectoryViewModel>();
@ -617,14 +606,10 @@ The gallery images are often inpainted, but you will get something very similar
public static PythonPackagesViewModel PythonPackagesViewModel =>
DialogFactory.Get<PythonPackagesViewModel>(vm =>
{
vm.AddPackages(
new PipPackageInfo("pip", "1.0.0"),
new PipPackageInfo("torch", "2.1.0+cu121")
);
vm.AddPackages(new PipPackageInfo("pip", "1.0.0"), new PipPackageInfo("torch", "2.1.0+cu121"));
});
public static LykosLoginViewModel LykosLoginViewModel =>
DialogFactory.Get<LykosLoginViewModel>();
public static LykosLoginViewModel LykosLoginViewModel => DialogFactory.Get<LykosLoginViewModel>();
public static OAuthConnectViewModel OAuthConnectViewModel =>
DialogFactory.Get<OAuthConnectViewModel>(vm =>
@ -647,15 +632,24 @@ The gallery images are often inpainted, but you will get something very similar
public static InferenceImageToImageViewModel InferenceImageToImageViewModel =>
DialogFactory.Get<InferenceImageToImageViewModel>();
public static InferenceImageUpscaleViewModel InferenceImageUpscaleViewModel =>
DialogFactory.Get<InferenceImageUpscaleViewModel>();
public static PackageImportViewModel PackageImportViewModel =>
DialogFactory.Get<PackageImportViewModel>();
public static PackageImportViewModel PackageImportViewModel => DialogFactory.Get<PackageImportViewModel>();
public static RefreshBadgeViewModel RefreshBadgeViewModel =>
new() { State = ProgressState.Success };
public static RefreshBadgeViewModel RefreshBadgeViewModel => new() { State = ProgressState.Success };
public static PropertyGridViewModel PropertyGridViewModel =>
DialogFactory.Get<PropertyGridViewModel>(vm =>
{
vm.SelectedObject = new INotifyPropertyChanged[]
{
new MockPropertyGridObject(),
new MockPropertyGridObjectAlt()
};
vm.ExcludeCategories = ["Excluded Category"];
});
public static SeedCardViewModel SeedCardViewModel => new();
@ -712,8 +706,7 @@ The gallery images are often inpainted, but you will get something very similar
);
});
public static ImageFolderCardViewModel ImageFolderCardViewModel =>
DialogFactory.Get<ImageFolderCardViewModel>();
public static ImageFolderCardViewModel ImageFolderCardViewModel => DialogFactory.Get<ImageFolderCardViewModel>();
public static FreeUCardViewModel FreeUCardViewModel => DialogFactory.Get<FreeUCardViewModel>();
@ -744,7 +737,7 @@ The gallery images are often inpainted, but you will get something very similar
public static StackEditableCardViewModel StackEditableCardViewModel =>
DialogFactory.Get<StackEditableCardViewModel>(vm =>
{
vm.AddCards(StackExpanderViewModel, StackExpanderViewModel);
vm.AddCards(StackExpanderViewModel, StackExpanderViewModel2);
});
public static StackExpanderViewModel StackExpanderViewModel =>
@ -755,11 +748,18 @@ The gallery images are often inpainted, but you will get something very similar
vm.OnContainerIndexChanged(0);
});
public static UpscalerCardViewModel UpscalerCardViewModel =>
DialogFactory.Get<UpscalerCardViewModel>();
public static StackExpanderViewModel StackExpanderViewModel2 =>
DialogFactory.Get<StackExpanderViewModel>(vm =>
{
vm.Title = "Hires Fix";
vm.IsSettingsEnabled = true;
vm.AddCards(UpscalerCardViewModel, SamplerCardViewModel);
vm.OnContainerIndexChanged(1);
});
public static BatchSizeCardViewModel BatchSizeCardViewModel =>
DialogFactory.Get<BatchSizeCardViewModel>();
public static UpscalerCardViewModel UpscalerCardViewModel => DialogFactory.Get<UpscalerCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel => DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModelWithIndexOption =>
DialogFactory.Get<BatchSizeCardViewModel>(vm =>
@ -811,14 +811,12 @@ The gallery images are often inpainted, but you will get something very similar
vm.Resource = ComfyUpscaler.DefaultDownloadableModels[0].DownloadableResource!.Value;
});
public static SharpenCardViewModel SharpenCardViewModel =>
DialogFactory.Get<SharpenCardViewModel>();
public static SharpenCardViewModel SharpenCardViewModel => DialogFactory.Get<SharpenCardViewModel>();
public static InferenceConnectionHelpViewModel InferenceConnectionHelpViewModel =>
DialogFactory.Get<InferenceConnectionHelpViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel =>
DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel => DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel_WithImage =>
DialogFactory.Get<SelectImageCardViewModel>(vm =>
@ -831,18 +829,13 @@ The gallery images are often inpainted, but you will get something very similar
});
public static ImageSource SampleImageSource =>
new(
new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"
)
)
new(new Uri("https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"))
{
Label = "Test Image"
};
public static ControlNetCardViewModel ControlNetCardViewModel =>
DialogFactory.Get<ControlNetCardViewModel>();
public static ControlNetCardViewModel ControlNetCardViewModel => DialogFactory.Get<ControlNetCardViewModel>();
public static Indexer Types { get; } = new();
public class Indexer
@ -851,9 +844,7 @@ The gallery images are often inpainted, but you will get something very similar
{
get
{
var type =
Type.GetType(typeName)
?? throw new ArgumentException($"Type {typeName} not found");
var type = Type.GetType(typeName) ?? throw new ArgumentException($"Type {typeName} not found");
try
{
return Services.GetService(type);

30
StabilityMatrix.Avalonia/DesignData/MockMetadataImportService.cs

@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.DesignData;
public class MockMetadataImportService : IMetadataImportService
{
public Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress<ProgressReport>? progress = null)
{
return Task.CompletedTask;
}
public Task<ConnectedModelInfo?> GetMetadataForFile(
FilePath filePath,
IProgress<ProgressReport>? progress = null,
bool forceReimport = false
)
{
return null;
}
public Task UpdateExistingMetadata(DirectoryPath directory, IProgress<ProgressReport>? progress = null)
{
return Task.CompletedTask;
}
}

54
StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs

@ -0,0 +1,54 @@
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using PropertyModels.ComponentModel;
using StabilityMatrix.Avalonia.Languages;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration
namespace StabilityMatrix.Avalonia.DesignData;
public partial class MockPropertyGridObject : ObservableObject
{
[ObservableProperty]
private string? stringProperty;
[ObservableProperty]
private int intProperty;
[ObservableProperty]
[property: Trackable(0, 50, Increment = 1, FormatString = "{0:0}")]
private int intRange = 10;
[ObservableProperty]
[property: Trackable(0d, 1d, Increment = 0.01, FormatString = "{0:P0}")]
private double floatPercentRange = 0.25;
[ObservableProperty]
[property: DisplayName("Int Custom Name")]
private int intCustomNameProperty = 42;
[ObservableProperty]
[property: DisplayName(nameof(Resources.Label_Language))]
private int? intLocalizedNameProperty;
[ObservableProperty]
private bool boolProperty;
[ObservableProperty]
[property: Category("Included Category")]
private string? stringIncludedCategoryProperty;
[ObservableProperty]
[property: Category("Excluded Category")]
private string? stringExcludedCategoryProperty;
}
public partial class MockPropertyGridObjectAlt : ObservableObject
{
[ObservableProperty]
private int altIntProperty = 10;
[ObservableProperty]
[property: Category("Settings")]
private string? altStringProperty;
}

2
StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs

@ -84,6 +84,8 @@ public static class TextEditorConfigs
textMate.SetGrammar(scope);
textMate.SetTheme(registryOptions.LoadTheme(ThemeName.DarkPlus));
editor.Options.ShowBoxForControlCharacters = false;
}
private static IRawTheme GetThemeFromStream(Stream stream)

11
StabilityMatrix.Avalonia/Helpers/ViewModelSerializer.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
@ -9,16 +8,12 @@ namespace StabilityMatrix.Avalonia.Helpers;
public static class ViewModelSerializer
{
public static IImmutableDictionary<string, Type> GetDerivedTypes(Type baseType)
public static Dictionary<string, Type> GetDerivedTypes(Type baseType)
{
return GetJsonDerivedTypeAttributes(baseType)
.ToImmutableDictionary(x => x.typeDiscriminator, x => x.subType);
return GetJsonDerivedTypeAttributes(baseType).ToDictionary(x => x.typeDiscriminator, x => x.subType);
}
public static IEnumerable<(
Type subType,
string typeDiscriminator
)> GetJsonDerivedTypeAttributes(Type type)
public static IEnumerable<(Type subType, string typeDiscriminator)> GetJsonDerivedTypeAttributes(Type type)
{
return type.GetCustomAttributes<JsonDerivedTypeAttribute>()
.Select(x => (x.DerivedType, x.TypeDiscriminator as string ?? x.DerivedType.Name));

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

@ -338,6 +338,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Open on Hugging Face.
/// </summary>
public static string Action_OpenOnHuggingFace {
get {
return ResourceManager.GetString("Action_OpenOnHuggingFace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Project....
/// </summary>
@ -563,6 +572,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Update Existing Metadata.
/// </summary>
public static string Action_UpdateExistingMetadata {
get {
return ResourceManager.GetString("Action_UpdateExistingMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Upgrade.
/// </summary>
@ -599,6 +617,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Addons.
/// </summary>
public static string Label_Addons {
get {
return ResourceManager.GetString("Label_Addons", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add Stability Matrix to the Start Menu.
/// </summary>
@ -662,6 +689,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Auto Completion.
/// </summary>
public static string Label_AutoCompletion {
get {
return ResourceManager.GetString("Label_AutoCompletion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Automatically scroll to end of console output.
/// </summary>
@ -761,6 +797,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to CivitAI.
/// </summary>
public static string Label_CivitAi {
get {
return ResourceManager.GetString("Label_CivitAi", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You must be logged in to download this checkpoint. Please enter a CivitAI API Key in the settings..
/// </summary>
@ -797,6 +842,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Replace underscores with spaces when inserting completions.
/// </summary>
public static string Label_CompletionReplaceUnderscoresWithSpaces {
get {
return ResourceManager.GetString("Label_CompletionReplaceUnderscoresWithSpaces", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Confirm Delete.
/// </summary>
@ -1023,7 +1077,7 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// Looks up a localized string similar to Emebeddings / Textual Inversion.
/// Looks up a localized string similar to Embeddings / Textual Inversion.
/// </summary>
public static string Label_EmbeddingsOrTextualInversion {
get {
@ -1112,6 +1166,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to General.
/// </summary>
public static string Label_General {
get {
return ResourceManager.GetString("Label_General", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height.
/// </summary>
@ -1121,6 +1184,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Hugging Face.
/// </summary>
public static string Label_HuggingFace {
get {
return ResourceManager.GetString("Label_HuggingFace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image to Image.
/// </summary>
@ -1130,6 +1202,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Image Viewer.
/// </summary>
public static string Label_ImageViewer {
get {
return ResourceManager.GetString("Label_ImageViewer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import with Metadata.
/// </summary>
@ -1166,6 +1247,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Inference.
/// </summary>
public static string Label_Inference {
get {
return ResourceManager.GetString("Label_Inference", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Inner exception.
/// </summary>
@ -1436,6 +1526,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Output Image Files.
/// </summary>
public static string Label_OutputImageFiles {
get {
return ResourceManager.GetString("Label_OutputImageFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Browser.
/// </summary>
@ -1562,6 +1661,42 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Prompt.
/// </summary>
public static string Label_Prompt {
get {
return ResourceManager.GetString("Label_Prompt", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Prompt Tags.
/// </summary>
public static string Label_PromptTags {
get {
return ResourceManager.GetString("Label_PromptTags", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tags file to use for suggesting completions (Supports the a1111-sd-webui-tagcomplete .csv format).
/// </summary>
public static string Label_PromptTagsDescription {
get {
return ResourceManager.GetString("Label_PromptTagsDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import Prompt tags.
/// </summary>
public static string Label_PromptTagsImport {
get {
return ResourceManager.GetString("Label_PromptTagsImport", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Python Packages.
/// </summary>
@ -1670,6 +1805,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Save Intermediate Image.
/// </summary>
public static string Label_SaveIntermediateImage {
get {
return ResourceManager.GetString("Label_SaveIntermediateImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search....
/// </summary>
@ -1697,6 +1841,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>
public static string Label_Settings {
get {
return ResourceManager.GetString("Label_Settings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shared Model Folder Strategy.
/// </summary>
@ -1814,6 +1967,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to System Information.
/// </summary>
public static string Label_SystemInformation {
get {
return ResourceManager.GetString("Label_SystemInformation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Text to Image.
/// </summary>

3
StabilityMatrix.Avalonia/Languages/Resources.es.resx

@ -680,4 +680,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restablecer Diseño Predeterminado</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx

@ -678,4 +678,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Rétablir la présentation par défaut</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.it-it.resx

@ -680,4 +680,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value></value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

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

@ -680,4 +680,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>レイアウトを初期状態に戻す</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

56
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -175,7 +175,7 @@
<value>Deemphasis</value>
</data>
<data name="Label_EmbeddingsOrTextualInversion" xml:space="preserve">
<value>Emebeddings / Textual Inversion</value>
<value>Embeddings / Textual Inversion</value>
</data>
<data name="Label_NetworksLoraOrLycoris" xml:space="preserve">
<value>Networks (Lora / LyCORIS)</value>
@ -825,4 +825,58 @@
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Open on Hugging Face</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Update Existing Metadata</value>
</data>
<data name="Label_General" xml:space="preserve">
<value>General</value><comment>A general settings category</comment>
</data>
<data name="Label_Inference" xml:space="preserve">
<value>Inference</value><comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Prompt</value><comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_OutputImageFiles" xml:space="preserve">
<value>Output Image Files</value>
</data>
<data name="Label_ImageViewer" xml:space="preserve">
<value>Image Viewer</value>
</data>
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Auto Completion</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Replace underscores with spaces when inserting completions</value>
</data>
<data name="Label_PromptTags" xml:space="preserve">
<value>Prompt Tags</value><comment>Tags for image generation prompts</comment>
</data>
<data name="Label_PromptTagsImport" xml:space="preserve">
<value>Import Prompt tags</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Tags file to use for suggesting completions (Supports the a1111-sd-webui-tagcomplete .csv format)</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>System Information</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Hugging Face</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Addons</value><comment>Inference Sampler Addons</comment>
</data>
<data name="Label_SaveIntermediateImage" xml:space="preserve">
<value>Save Intermediate Image</value><comment>Inference module step to save an intermediate image</comment>
</data>
<data name="Label_Settings" xml:space="preserve">
<value>Settings</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx

@ -678,4 +678,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Восстановить вид по умолчанию</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx

@ -647,4 +647,7 @@
<data name="Label_Branch" xml:space="preserve">
<value>分支</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx

@ -639,4 +639,7 @@
<data name="Label_Branch" xml:space="preserve">
<value>分支</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

46
StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs

@ -0,0 +1,46 @@
using System.Text.Json.Serialization;
using StabilityMatrix.Core.Converters.Json;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Models.HuggingFace;
[JsonConverter(typeof(DefaultUnknownEnumConverter<HuggingFaceModelType>))]
public enum HuggingFaceModelType
{
[Description("Base Models")]
[ConvertTo<SharedFolderType>(SharedFolderType.StableDiffusion)]
BaseModel,
[Description("ControlNets")]
[ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)]
ControlNet,
[Description("ControlNets (Diffusers)")]
[ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)]
DiffusersControlNet,
[Description("IP Adapters")]
[ConvertTo<SharedFolderType>(SharedFolderType.IpAdapter)]
IpAdapter,
[Description("IP Adapters (Diffusers SD1.5)")]
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeIpAdapters15)]
DiffusersIpAdapter,
[Description("IP Adapters (Diffusers SDXL)")]
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeIpAdaptersXl)]
DiffusersIpAdapterXl,
[Description("CLIP Vision (Diffusers)")]
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeClipVision)]
DiffusersClipVision,
[Description("T2I Adapters")]
[ConvertTo<SharedFolderType>(SharedFolderType.T2IAdapter)]
T2IAdapter,
[Description("T2I Adapters (Diffusers)")]
[ConvertTo<SharedFolderType>(SharedFolderType.T2IAdapter)]
DiffusersT2IAdapter,
}

12
StabilityMatrix.Avalonia/Models/HuggingFace/HuggingfaceItem.cs

@ -0,0 +1,12 @@
namespace StabilityMatrix.Avalonia.Models.HuggingFace;
public class HuggingfaceItem
{
public required HuggingFaceModelType ModelCategory { get; set; }
public required string ModelName { get; set; }
public required string RepositoryPath { get; set; }
public required string[] Files { get; set; }
public required string LicenseType { get; set; }
public string? LicensePath { get; set; }
public string? Subfolder { get; set; }
}

4
StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs

@ -40,7 +40,7 @@ public class ModuleApplyStepEventArgs : EventArgs
public (
ConditioningNodeConnection Positive,
ConditioningNodeConnection Negative
) Conditioning { get; set; }
)? Conditioning { get; set; }
/// <summary>
/// Temporary refiner conditioning apply step, used by samplers to apply control net.
@ -48,7 +48,7 @@ public class ModuleApplyStepEventArgs : EventArgs
public (
ConditioningNodeConnection Positive,
ConditioningNodeConnection Negative
) RefinerConditioning { get; set; }
)? RefinerConditioning { get; set; }
/// <summary>
/// Temporary model apply step, used by samplers to apply control net.

55
StabilityMatrix.Avalonia/Program.cs

@ -24,6 +24,7 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia;
@ -53,7 +54,8 @@ public static class Program
SetDebugBuild();
var parseResult = Parser.Default
var parseResult = Parser
.Default
.ParseArguments<AppArgs>(args)
.WithNotParsed(errors =>
{
@ -68,6 +70,7 @@ public static class Program
if (Args.HomeDirectoryOverride is { } homeDir)
{
Compat.SetAppDataHome(homeDir);
GlobalConfig.HomeDir = homeDir;
}
// Launched for custom URI scheme, handle and exit
@ -77,11 +80,7 @@ public static class Program
{
if (
Uri.TryCreate(uriArg, UriKind.Absolute, out var uri)
&& string.Equals(
uri.Scheme,
UriHandler.Scheme,
StringComparison.OrdinalIgnoreCase
)
&& string.Equals(uri.Scheme, UriHandler.Scheme, StringComparison.OrdinalIgnoreCase)
)
{
UriHandler.SendAndExit(uri);
@ -147,20 +146,10 @@ public static class Program
if (Args.DisableGpuRendering)
{
app = app.With(
new Win32PlatformOptions
{
RenderingMode = new[] { Win32RenderingMode.Software }
}
)
.With(
new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } }
)
app = app.With(new Win32PlatformOptions { RenderingMode = new[] { Win32RenderingMode.Software } })
.With(new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } })
.With(
new AvaloniaNativePlatformOptions
{
RenderingMode = new[] { AvaloniaNativeRenderingMode.Software }
}
new AvaloniaNativePlatformOptions { RenderingMode = new[] { AvaloniaNativeRenderingMode.Software } }
);
}
@ -257,10 +246,7 @@ public static class Program
try
{
var process = Process.GetProcessById(pid);
process
.WaitForExitAsync(new CancellationTokenSource(timeout).Token)
.GetAwaiter()
.GetResult();
process.WaitForExitAsync(new CancellationTokenSource(timeout).Token).GetAwaiter().GetResult();
}
catch (OperationCanceledException)
{
@ -281,8 +267,7 @@ public static class Program
{
SentrySdk.Init(o =>
{
o.Dsn =
"https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328";
o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328";
o.StackTraceMode = StackTraceMode.Enhanced;
o.TracesSampleRate = 1.0;
o.IsGlobalModeEnabled = true;
@ -309,10 +294,7 @@ public static class Program
});
}
private static void TaskScheduler_UnobservedTaskException(
object? sender,
UnobservedTaskExceptionEventArgs e
)
private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
if (e.Exception is Exception ex)
{
@ -320,10 +302,7 @@ public static class Program
}
}
private static void CurrentDomain_UnhandledException(
object sender,
UnhandledExceptionEventArgs e
)
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is not Exception ex)
return;
@ -340,15 +319,9 @@ public static class Program
Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message);
}
if (
Application.Current?.ApplicationLifetime
is IClassicDesktopStyleApplicationLifetime lifetime
)
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
var dialog = new ExceptionDialog
{
DataContext = new ExceptionViewModel { Exception = ex }
};
var dialog = new ExceptionDialog { DataContext = new ExceptionViewModel { Exception = ex } };
var mainWindow = lifetime.MainWindow;
// We can only show dialog if main window exists, and is visible

5
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -8,7 +8,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.7.0-dev.4</Version>
<Version>2.7.0-pre.999</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -32,6 +32,8 @@
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.5" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.5" />
<PackageReference Include="bodong.Avalonia.PropertyGrid" Version="11.0.5.1" />
<PackageReference Include="bodong.PropertyModels" Version="11.0.5.1" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
@ -79,6 +81,7 @@
<ItemGroup>
<AvaloniaResource Include="Assets\Icon.ico" />
<AvaloniaResource Include="Assets\Icon.png" />
<AvaloniaResource Include="Assets\hf-packages.json" />
</ItemGroup>
<ItemGroup>

4
StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml

@ -377,8 +377,8 @@
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />

46
StabilityMatrix.Avalonia/Styles/ProgressRing.axaml

@ -7,10 +7,13 @@
<Design.PreviewWith>
<StackPanel>
<Border Padding="20">
<controls:ProgressRing StartAngle="90" EndAngle="270" Value="50" Foreground="Red" BorderThickness="5" Width="100" Height="100" />
<controls:ProgressRing Value="50" />
</Border>
<Border Padding="20">
<controls:ProgressRing IsIndeterminate="True" BorderThickness="10" Width="100" Height="100" />
<controls:ProgressRing StartAngle="90" EndAngle="450" Value="50" Foreground="Red" StrokeThickness="3" Width="100" Height="100" />
</Border>
<Border Padding="20">
<controls:ProgressRing IsIndeterminate="True" StrokeThickness="10" Width="100" Height="100" />
</Border>
</StackPanel>
</Design.PreviewWith>
@ -22,27 +25,40 @@
<Style Selector="controls|ProgressRing">
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="BorderThickness" Value="9" />
<Setter Property="StrokeThickness" Value="5" />
<Setter Property="MinHeight" Value="16" />
<Setter Property="MinWidth" Value="16" />
<Setter Property="Transitions">
<Transitions>
<DoubleTransition
Property="SweepAngle"
Duration="0:0:0.267">
<DoubleTransition.Easing>
<LinearEasing/>
</DoubleTransition.Easing>
</DoubleTransition>
</Transitions>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Panel x:Name="FluentRingRoot">
<Ellipse x:Name="Track"
<Panel x:Name="PART_RingRoot">
<Ellipse x:Name="PART_Track"
Stroke="{TemplateBinding Background}"
StrokeThickness="{Binding BorderThickness.Left, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
<Arc x:Name="Fill"
StrokeThickness="{TemplateBinding StrokeThickness}" />
<Arc x:Name="PART_Fill"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="{Binding BorderThickness.Left, RelativeSource={RelativeSource Mode=TemplatedParent}}"
StrokeThickness="{TemplateBinding StrokeThickness}"
StrokeLineCap="Round" />
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="controls|ProgressRing:not(:indeterminate) /template/ Arc#Fill">
<Setter Property="StartAngle" Value="{Binding StartAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
<Setter Property="SweepAngle" Value="{Binding ValueAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
<Style Selector="controls|ProgressRing:not(:indeterminate) /template/ Arc#PART_Fill">
<!--<Setter Property="StartAngle" Value="{Binding StartAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>-->
<!--<Setter Property="SweepAngle" Value="{Binding SweepAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>-->
</Style>
<Style Selector="controls|ProgressRing:preserveaspect">
@ -50,14 +66,14 @@
<Setter Property="MinHeight" Value="32" />
</Style>
<Style Selector="controls|ProgressRing:preserveaspect /template/ Panel#FluentRingRoot">
<Style Selector="controls|ProgressRing:preserveaspect /template/ Panel#PART_RingRoot">
<Setter Property="Width" Value="{TemplateBinding Bounds, Converter={StaticResource FitSquarelyWithinAspectRatioConverter}}"/>
<Setter Property="Height" Value="{Binding Width, RelativeSource={RelativeSource Mode=Self}}"/>
</Style>
<Style Selector="controls|ProgressRing[IsEnabled=True]:indeterminate /template/ Arc#Fill">
<Style Selector="controls|ProgressRing[IsEnabled=True]:indeterminate /template/ Arc#PART_Fill">
<Style.Animations>
<Animation Duration="0:0:5" Easing="LinearEasing" IterationCount="INFINITE" FillMode="Both">
<Animation Duration="0:0:4.567" Easing="LinearEasing" IterationCount="Infinite" FillMode="Both">
<KeyFrame Cue="0%">
<Setter Property="StartAngle" Value="-720"/>
<Setter Property="SweepAngle" Value="0"/>
@ -98,7 +114,7 @@
</Style.Animations>
</Style>
<Style Selector="controls|ProgressRing[IsEnabled=True] /template/ Ellipse#Track">
<Style Selector="controls|ProgressRing[IsEnabled=True]:not(:indeterminate) /template/ Ellipse#PART_Track">
<Style.Animations>
<Animation Duration="0:0:1" IterationCount="INFINITE">
<KeyFrame Cue="0%">

63
StabilityMatrix.Avalonia/Styles/SplitButtonStyles.axaml

@ -11,6 +11,7 @@
<SplitButton Classes="info" Content="Info Button" Margin="8" HorizontalAlignment="Center" />
<SplitButton Classes="transparent-info" Content="Semi-Transparent Info Button" Margin="8" HorizontalAlignment="Center" />
<SplitButton Classes="transparent" Content="Transparent Button" Margin="8" HorizontalAlignment="Center" />
<SplitButton Classes="transparent-full" Content="Full Transparent Button" Margin="8" HorizontalAlignment="Center" />
<SplitButton Content="Disabled Button" Margin="8" IsEnabled="False" HorizontalAlignment="Center" />
</StackPanel>
</Border>
@ -417,6 +418,68 @@
</Style>
</Style>
<!-- Transparent-Full -->
<Style Selector="SplitButton.transparent-full">
<Style Selector="^ /template/ Button /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="^ /template/ Button /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<!-- Pseudo-classes -->
<Style Selector="^ /template/ Button#PART_PrimaryButton:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^ /template/ Button#PART_SecondaryButton:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^ /template/ Button#PART_PrimaryButton:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^ /template/ Button#PART_SecondaryButton:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Button /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Semi-Transparent Info -->
<Style Selector="SplitButton.transparent-info">
<Style Selector="^ /template/ Button /template/ ui|FABorder#Root">

22
StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs

@ -1,27 +1,43 @@
using System;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public class ContentDialogViewModelBase : ViewModelBase
{
public virtual string? Title { get; set; }
// Events for button clicks
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);
}
/// <summary>
/// Return a <see cref="BetterContentDialog"/> that uses this view model as its content
/// </summary>
public virtual BetterContentDialog GetDialog()
{
Dispatcher.UIThread.VerifyAccess();
var dialog = new BetterContentDialog { Title = Title, Content = this };
return dialog;
}
}

139
StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@ -41,9 +42,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Base;
/// This includes a progress reporter, image output view model, and generation virtual methods.
/// </summary>
[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")]
public abstract partial class InferenceGenerationViewModelBase
: InferenceTabViewModelBase,
IImageGalleryComponent
public abstract partial class InferenceGenerationViewModelBase : InferenceTabViewModelBase, IImageGalleryComponent
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -98,14 +97,7 @@ public abstract partial class InferenceGenerationViewModelBase
var defaultOutputDir = settingsManager.ImagesInferenceDirectory;
defaultOutputDir.Create();
return WriteOutputImageAsync(
imageStream,
defaultOutputDir,
args,
batchNum,
batchTotal,
isGrid
);
return WriteOutputImageAsync(imageStream, defaultOutputDir, args, batchNum, batchTotal, isGrid);
}
/// <summary>
@ -136,10 +128,7 @@ public abstract partial class InferenceGenerationViewModelBase
)
{
// Fallback to default
Logger.Warn(
"Failed to parse format template: {FormatTemplate}, using default",
formatTemplateStr
);
Logger.Warn("Failed to parse format template: {FormatTemplate}, using default", formatTemplateStr);
format = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, formatProvider);
}
@ -200,11 +189,7 @@ public abstract partial class InferenceGenerationViewModelBase
{
var uploadName = await image.GetHashGuidFileNameAsync();
Logger.Debug(
"Uploading image {FileName} as {UploadName}",
localFile.Name,
uploadName
);
Logger.Debug("Uploading image {FileName} as {UploadName}", localFile.Name, uploadName);
// For pngs, strip metadata since Pillow can't handle some valid files?
if (localFile.Info.Extension.Equals(".png", StringComparison.OrdinalIgnoreCase))
@ -228,10 +213,7 @@ public abstract partial class InferenceGenerationViewModelBase
/// Runs a generation task
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if args.Parameters or args.Project are null</exception>
protected async Task RunGeneration(
ImageGenerationEventArgs args,
CancellationToken cancellationToken
)
protected async Task RunGeneration(ImageGenerationEventArgs args, CancellationToken cancellationToken)
{
var client = args.Client;
var nodes = args.Nodes;
@ -311,29 +293,18 @@ public abstract partial class InferenceGenerationViewModelBase
{
Logger.Warn(e, "Comfy node exception while queuing prompt");
await DialogHelper
.CreateJsonDialog(
e.JsonData,
"Comfy Error",
"Node execution encountered an error"
)
.CreateJsonDialog(e.JsonData, "Comfy Error", "Node execution encountered an error")
.ShowAsync();
return;
}
// Get output images
var imageOutputs = await client.GetImagesForExecutedPromptAsync(
promptTask.Id,
cancellationToken
);
var imageOutputs = await client.GetImagesForExecutedPromptAsync(promptTask.Id, cancellationToken);
if (imageOutputs.Values.All(images => images is null or { Count: 0 }))
{
// No images match
notificationService.Show(
"No output",
"Did not receive any output images",
NotificationType.Warning
);
notificationService.Show("No output", "Did not receive any output images", NotificationType.Warning);
return;
}
@ -410,10 +381,7 @@ public abstract partial class InferenceGenerationViewModelBase
var project = args.Project!;
// Lock seed
project.TryUpdateModel<SeedCardModel>(
"Seed",
model => model with { IsRandomizeEnabled = false }
);
project.TryUpdateModel<SeedCardModel>("Seed", model => model with { IsRandomizeEnabled = false });
// Seed and batch override for batches
if (images.Count > 1 && project.ProjectType is InferenceProjectType.TextToImage)
@ -436,12 +404,7 @@ public abstract partial class InferenceGenerationViewModelBase
var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project);
// Write using generated name
var filePath = await WriteOutputImageAsync(
new MemoryStream(bytesWithMetadata),
args,
i + 1,
images.Count
);
var filePath = await WriteOutputImageAsync(new MemoryStream(bytesWithMetadata), args, i + 1, images.Count);
outputImages.Add(new ImageSource(filePath) { Label = imageLabel });
@ -456,25 +419,14 @@ public abstract partial class InferenceGenerationViewModelBase
var project = args.Project!;
// Lock seed
project.TryUpdateModel<SeedCardModel>(
"Seed",
model => model with { IsRandomizeEnabled = false }
);
project.TryUpdateModel<SeedCardModel>("Seed", model => model with { IsRandomizeEnabled = false });
var grid = ImageProcessor.CreateImageGrid(loadedImages);
var gridBytes = grid.Encode().ToArray();
var gridBytesWithMetadata = PngDataHelper.AddMetadata(
gridBytes,
args.Parameters!,
args.Project!
);
var gridBytesWithMetadata = PngDataHelper.AddMetadata(gridBytes, args.Parameters!, args.Project!);
// Save to disk
var gridPath = await WriteOutputImageAsync(
new MemoryStream(gridBytesWithMetadata),
args,
isGrid: true
);
var gridPath = await WriteOutputImageAsync(new MemoryStream(gridBytesWithMetadata), args, isGrid: true);
// Insert to start of images
var gridImage = new ImageSource(gridPath) { Label = imageLabel };
@ -498,10 +450,7 @@ public abstract partial class InferenceGenerationViewModelBase
/// <summary>
/// Implementation for Generate Image
/// </summary>
protected virtual Task GenerateImageImpl(
GenerateOverrides overrides,
CancellationToken cancellationToken
)
protected virtual Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
@ -512,10 +461,7 @@ public abstract partial class InferenceGenerationViewModelBase
/// <param name="options">Optional overrides (side buttons)</param>
/// <param name="cancellationToken">Cancellation token</param>
[RelayCommand(IncludeCancelCommand = true, FlowExceptionsToTaskScheduler = true)]
private async Task GenerateImage(
GenerateFlags options = default,
CancellationToken cancellationToken = default
)
private async Task GenerateImage(GenerateFlags options = default, CancellationToken cancellationToken = default)
{
var overrides = GenerateOverrides.FromFlags(options);
@ -525,7 +471,12 @@ public abstract partial class InferenceGenerationViewModelBase
}
catch (OperationCanceledException)
{
Logger.Debug($"Image Generation Canceled");
Logger.Debug("Image Generation Canceled");
}
catch (ValidationException e)
{
Logger.Debug("Image Generation Validation Error: {Message}", e.Message);
notificationService.Show("Validation Error", e.Message, NotificationType.Error);
}
}
@ -556,21 +507,19 @@ public abstract partial class InferenceGenerationViewModelBase
/// Handles the progress update received event from the websocket.
/// Updates the progress view model.
/// </summary>
protected virtual void OnProgressUpdateReceived(
object? sender,
ComfyProgressUpdateEventArgs args
)
protected virtual void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args)
{
Dispatcher.UIThread.Post(() =>
{
OutputProgress.Value = args.Value;
OutputProgress.Maximum = args.Maximum;
OutputProgress.IsIndeterminate = false;
Dispatcher
.UIThread
.Post(() =>
{
OutputProgress.Value = args.Value;
OutputProgress.Maximum = args.Maximum;
OutputProgress.IsIndeterminate = false;
OutputProgress.Text =
$"({args.Value} / {args.Maximum})"
+ (args.RunningNode != null ? $" {args.RunningNode}" : "");
});
OutputProgress.Text =
$"({args.Value} / {args.Maximum})" + (args.RunningNode != null ? $" {args.RunningNode}" : "");
});
}
private void AttachRunningNodeChangedHandler(ComfyTask comfyTask)
@ -595,13 +544,15 @@ public abstract partial class InferenceGenerationViewModelBase
return;
}
Dispatcher.UIThread.Post(() =>
{
OutputProgress.IsIndeterminate = true;
OutputProgress.Value = 100;
OutputProgress.Maximum = 100;
OutputProgress.Text = nodeName;
});
Dispatcher
.UIThread
.Post(() =>
{
OutputProgress.IsIndeterminate = true;
OutputProgress.Value = 100;
OutputProgress.Maximum = 100;
OutputProgress.Text = nodeName;
});
}
public class ImageGenerationEventArgs : EventArgs
@ -629,11 +580,7 @@ public abstract partial class InferenceGenerationViewModelBase
overrides[typeof(HiresFixModule)] = args.Overrides.IsHiresFixEnabled.Value;
}
return new ModuleApplyStepEventArgs
{
Builder = args.Builder,
IsEnabledOverrides = overrides
};
return new ModuleApplyStepEventArgs { Builder = args.Builder, IsEnabledOverrides = overrides };
}
}
}

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

@ -0,0 +1,6 @@
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public abstract class TabViewModelBase : ViewModelBase
{
public abstract string Header { get; }
}

10
StabilityMatrix.Avalonia/ViewModels/Base/TaskDialogViewModelBase.cs

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Threading;
using AvaloniaEdit.Rendering;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Languages;
@ -13,6 +14,8 @@ public abstract class TaskDialogViewModelBase : ViewModelBase
{
private TaskDialog? dialog;
public virtual string? Title { get; set; }
protected static TaskDialogCommand GetCommandButton(string text, ICommand command)
{
return new TaskDialogCommand
@ -27,11 +30,7 @@ public abstract class TaskDialogViewModelBase : ViewModelBase
protected static TaskDialogButton GetCloseButton()
{
return new TaskDialogButton
{
Text = Resources.Action_Close,
DialogResult = TaskDialogStandardResult.Close
};
return new TaskDialogButton { Text = Resources.Action_Close, DialogResult = TaskDialogStandardResult.Close };
}
/// <summary>
@ -43,6 +42,7 @@ public abstract class TaskDialogViewModelBase : ViewModelBase
dialog = new TaskDialog
{
Header = Title,
Content = this,
XamlRoot = App.VisualRoot,
Buttons = { GetCloseButton() }

612
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs

@ -0,0 +1,612 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LiteDB;
using LiteDB.Async;
using NLog;
using Refit;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[View(typeof(CivitAiBrowserPage))]
[Singleton]
public partial class CivitAiBrowserViewModel : TabViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 20;
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 List<string> BaseModelOptions => new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" };
public CivitAiBrowserViewModel(
ICivitApi civitApi,
IDownloadService downloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
ILiteDbContext liteDbContext,
INotificationService notificationService
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext;
this.notificationService = notificationService;
CurrentPageNumber = 1;
CanGoToNextPage = true;
CanGoToLastPage = true;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select<EventPattern<PropertyChangedEventArgs>, int>(_ => CurrentPageNumber)
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
var searchOptions = settingsManager.Settings.ModelSearchOptions;
// Fix SelectedModelType if someone had selected the obsolete "Model" option
if (searchOptions is { SelectedModelType: CivitModelType.Model })
{
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
CivitModelType.Checkpoint,
SelectedBaseModelType
)
);
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);
}
/// <summary>
/// Filter predicate for model cards
/// </summary>
private bool FilterModelCardsPredicate(object? item)
{
if (item is not CheckpointBrowserCardViewModel card)
return false;
return !card.CivitModel.Nsfw || ShowNsfw;
}
/// <summary>
/// Background update task
/// </summary>
private async Task CivitModelQuery(CivitModelsRequest request)
{
var timer = Stopwatch.StartNew();
var queryText = request.Query;
try
{
var modelsResponse = await civitApi.GetModels(request);
var models = modelsResponse.Items;
if (models is null)
{
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
);
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);
}
// 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();
// 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
}
);
if (cacheNew)
{
Logger.Debug("New cache entry, updating model cards");
UpdateModelCards(models, modelsResponse.Metadata);
}
else
{
Logger.Debug("Cache entry already exists, not updating model cards");
}
}
catch (OperationCanceledException)
{
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")
);
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")
);
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}"
)
);
Logger.Error(e, $"CivitAI query unknown exception ({request})");
}
finally
{
ShowMainLoadingSpinner = false;
UpdateResultsText();
}
}
/// <summary>
/// Updates model cards using api response object.
/// </summary>
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata)
{
if (models is null)
{
ModelCards?.Clear();
}
else
{
var updateCards = models
.Select(model =>
{
var cachedViewModel = cache.Get(model.Id);
if (cachedViewModel != null)
{
if (!cachedViewModel.IsImporting)
{
cache.Remove(model.Id);
}
return cachedViewModel;
}
var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{
if (cache.Get(viewModel.CivitModel.Id) != null)
return;
cache.Add(viewModel.CivitModel.Id, viewModel);
};
return vm;
});
return newCard;
})
.ToList();
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed)
{
filteredCards = filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available");
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
}
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
CanGoToLastPage = CurrentPageNumber != TotalPages;
// Status update
ShowMainLoadingSpinner = false;
IsIndeterminate = false;
HasSearched = true;
}
private string previousSearchQuery = string.Empty;
[RelayCommand]
private async Task SearchModels()
{
var timer = Stopwatch.StartNew();
if (SearchQuery != previousSearchQuery)
{
// Reset page number
CurrentPageNumber = 1;
previousSearchQuery = SearchQuery;
}
// Build request
var modelRequest = new CivitModelsRequest
{
Limit = MaxModelsPerPage,
Nsfw = "true", // Handled by local view filter
Sort = SortMode,
Period = SelectedPeriod,
Page = CurrentPageNumber
};
if (SearchQuery.StartsWith("#"))
{
modelRequest.Tag = SearchQuery[1..];
}
else if (SearchQuery.StartsWith("@"))
{
modelRequest.Username = SearchQuery[1..];
}
else
{
modelRequest.Query = SearchQuery;
}
if (SelectedModelType != CivitModelType.All)
{
modelRequest.Types = new[] { SelectedModelType };
}
if (SelectedBaseModelType != "All")
{
modelRequest.BaseModel = SelectedBaseModelType;
}
if (SortMode == CivitSortMode.Installed)
{
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
modelRequest.CommaSeparatedModelIds = string.Join(
",",
connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m).Select(g => g.First())
);
modelRequest.Sort = null;
modelRequest.Period = null;
}
else if (SortMode == CivitSortMode.Favorites)
{
var favoriteModels = settingsManager.Settings.FavoriteModels;
if (!favoriteModels.Any())
{
notificationService.Show(
"No Favorites",
"You have not added any models to your Favorites.",
NotificationType.Error
);
return;
}
modelRequest.CommaSeparatedModelIds = string.Join(",", favoriteModels);
modelRequest.Sort = null;
modelRequest.Period = null;
}
// See if query is cached
CivitModelQueryCacheEntry? cachedQuery = null;
try
{
cachedQuery = await liteDbContext
.CivitModelQueryCache
.IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
}
catch (Exception e)
{
// Suppress 'Training_Data' enum not found exceptions
// Caused by enum name change
// Ignore to do a new search to overwrite the cache
if (
!(
e is LiteException or LiteAsyncException
&& e.InnerException is ArgumentException inner
&& inner.Message.Contains("Training_Data")
)
)
{
// Otherwise log error
Logger.Error(e, "Error while querying CivitModelQueryCache");
}
}
// 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
);
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata);
// Start remote query (background mode)
// Skip when last query was less than 2 min ago
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt;
if (timeSinceCache?.TotalMinutes >= 2)
{
CivitModelQuery(modelRequest).SafeFireAndForget();
Logger.Debug(
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query",
timeSinceCache.Value.TotalSeconds
);
}
}
else
{
// Not cached, wait for remote query
ShowMainLoadingSpinner = true;
await CivitModelQuery(modelRequest);
}
UpdateResultsText();
}
public void FirstPage()
{
CurrentPageNumber = 1;
}
public void PreviousPage()
{
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
public void LastPage()
{
CurrentPageNumber = TotalPages;
}
public void ClearSearchQuery()
{
SearchQuery = string.Empty;
}
partial void OnShowNsfwChanged(bool value)
{
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value);
// ModelCardsView?.Refresh();
var updateCards = allModelCards.Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);
if (!HasSearched)
return;
UpdateResultsText();
}
partial void OnSelectedPeriodChanged(CivitPeriod value)
{
TrySearchAgain().SafeFireAndForget();
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
)
);
}
partial void OnSelectedModelTypeChanged(CivitModelType value)
{
TrySearchAgain().SafeFireAndForget();
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)
);
}
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)
{
if (!HasSearched)
return;
ModelCards?.Clear();
if (shouldUpdatePageNumber)
{
CurrentPageNumber = 1;
}
// execute command instead of calling method directly so that the IsRunning property gets updated
await SearchModelsCommand.ExecuteAsync(null);
}
private void UpdateResultsText()
{
NoResultsFound = ModelCards?.Count <= 0;
NoResultsText =
allModelCards.Count > 0 ? $"{allModelCards.Count} results hidden by filters" : "No results found";
}
public override string Header => Resources.Label_CivitAi;
}

193
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/HuggingFacePageViewModel.cs

@ -0,0 +1,193 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models.HuggingFace;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.HuggingFacePage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[View(typeof(Views.HuggingFacePage))]
[Singleton]
public partial class HuggingFacePageViewModel : TabViewModelBase
{
private readonly ITrackedDownloadService trackedDownloadService;
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
public SourceCache<HuggingfaceItem, string> ItemsCache { get; } = new(i => i.RepositoryPath + i.ModelName);
public IObservableCollection<CategoryViewModel> Categories { get; set; } =
new ObservableCollectionExtended<CategoryViewModel>();
public string DownloadPercentText =>
Math.Abs(TotalProgress.Percentage - 100f) < 0.001f
? "Download Complete"
: $"Downloading {TotalProgress.Percentage:0}%";
[ObservableProperty]
private int numSelected;
private ConcurrentDictionary<Guid, ProgressReport> progressReports = new();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DownloadPercentText))]
private ProgressReport totalProgress;
private readonly DispatcherTimer progressTimer = new() { Interval = TimeSpan.FromMilliseconds(100) };
public HuggingFacePageViewModel(
ITrackedDownloadService trackedDownloadService,
ISettingsManager settingsManager,
INotificationService notificationService
)
{
this.trackedDownloadService = trackedDownloadService;
this.settingsManager = settingsManager;
this.notificationService = notificationService;
ItemsCache
.Connect()
.DeferUntilLoaded()
.Group(i => i.ModelCategory)
.Transform(g => new CategoryViewModel(g.Cache.Items) { Title = g.Key.GetDescription() ?? g.Key.ToString() })
.SortBy(vm => vm.Title)
.Bind(Categories)
.WhenAnyPropertyChanged()
.Subscribe(_ => NumSelected = Categories.Sum(c => c.NumSelected));
progressTimer.Tick += (_, _) =>
{
var currentSum = 0ul;
var totalSum = 0ul;
foreach (var progress in progressReports.Values)
{
currentSum += progress.Current ?? 0;
totalSum += progress.Total ?? 0;
}
TotalProgress = new ProgressReport(current: currentSum, total: totalSum);
};
}
public override void OnLoaded()
{
if (ItemsCache.Count > 0)
return;
using var reader = new StreamReader(Assets.HfPackagesJson.Open());
var packages =
JsonSerializer.Deserialize<IReadOnlyList<HuggingfaceItem>>(
reader.ReadToEnd(),
new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }
) ?? throw new InvalidOperationException("Failed to read hf-packages.json");
ItemsCache.EditDiff(packages, (a, b) => a.RepositoryPath == b.RepositoryPath);
}
public void ClearSelection()
{
foreach (var category in Categories)
{
category.IsChecked = true;
category.IsChecked = false;
}
}
public void SelectAll()
{
foreach (var category in Categories)
{
category.IsChecked = true;
}
}
[RelayCommand]
private async Task ImportSelected()
{
var selected = Categories.SelectMany(c => c.Items).Where(i => i.IsSelected).ToArray();
foreach (var viewModel in selected)
{
foreach (var file in viewModel.Item.Files)
{
var url = $"https://huggingface.co/{viewModel.Item.RepositoryPath}/resolve/main/{file}?download=true";
var sharedFolderType = viewModel.Item.ModelCategory.ConvertTo<SharedFolderType>();
var downloadPath = new FilePath(
Path.Combine(
settingsManager.ModelsDirectory,
sharedFolderType.GetDescription() ?? sharedFolderType.ToString(),
viewModel.Item.Subfolder ?? string.Empty,
file
)
);
Directory.CreateDirectory(downloadPath.Directory);
var download = trackedDownloadService.NewDownload(url, downloadPath);
download.ProgressUpdate += DownloadOnProgressUpdate;
download.Start();
await Task.Delay(Random.Shared.Next(50, 100));
}
}
progressTimer.Start();
}
private void DownloadOnProgressUpdate(object? sender, ProgressReport e)
{
if (sender is not TrackedDownload trackedDownload)
return;
progressReports[trackedDownload.Id] = e;
}
partial void OnTotalProgressChanged(ProgressReport value)
{
if (Math.Abs(value.Percentage - 100) < 0.001f)
{
notificationService.Show(
"Download complete",
"All selected models have been downloaded.",
NotificationType.Success
);
progressTimer.Stop();
DelayedClearProgress(TimeSpan.FromSeconds(1.5));
}
}
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay)
.ContinueWith(_ =>
{
TotalProgress = new ProgressReport(0, 0);
});
}
public override string Header => Resources.Label_HuggingFace;
}

654
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -1,40 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using LiteDB;
using LiteDB.Async;
using NLog;
using Refit;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -42,621 +15,18 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(CheckpointBrowserPage))]
[Singleton]
public partial class CheckpointBrowserViewModel : PageViewModelBase
public partial class CheckpointBrowserViewModel(
CivitAiBrowserViewModel civitAiBrowserViewModel,
HuggingFacePageViewModel huggingFaceViewModel
) : PageViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 20;
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 List<string> BaseModelOptions =>
new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" };
public CheckpointBrowserViewModel(
ICivitApi civitApi,
IDownloadService downloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
ILiteDbContext liteDbContext,
INotificationService notificationService
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext;
this.notificationService = notificationService;
CurrentPageNumber = 1;
CanGoToNextPage = true;
CanGoToLastPage = true;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select(_ => CurrentPageNumber)
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
var searchOptions = settingsManager.Settings.ModelSearchOptions;
// Fix SelectedModelType if someone had selected the obsolete "Model" option
if (searchOptions is { SelectedModelType: CivitModelType.Model })
{
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
CivitModelType.Checkpoint,
SelectedBaseModelType
)
);
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
);
}
/// <summary>
/// Filter predicate for model cards
/// </summary>
private bool FilterModelCardsPredicate(object? item)
{
if (item is not CheckpointBrowserCardViewModel card)
return false;
return !card.CivitModel.Nsfw || ShowNsfw;
}
/// <summary>
/// Background update task
/// </summary>
private async Task CivitModelQuery(CivitModelsRequest request)
{
var timer = Stopwatch.StartNew();
var queryText = request.Query;
try
{
var modelsResponse = await civitApi.GetModels(request);
var models = modelsResponse.Items;
if (models is null)
{
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
);
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
);
}
// 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();
// 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
}
);
if (cacheNew)
{
Logger.Debug("New cache entry, updating model cards");
UpdateModelCards(models, modelsResponse.Metadata);
}
else
{
Logger.Debug("Cache entry already exists, not updating model cards");
}
}
catch (OperationCanceledException)
{
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"
)
);
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"
)
);
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}"
)
);
Logger.Error(e, $"CivitAI query unknown exception ({request})");
}
finally
{
ShowMainLoadingSpinner = false;
UpdateResultsText();
}
}
/// <summary>
/// Updates model cards using api response object.
/// </summary>
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata)
{
if (models is null)
{
ModelCards?.Clear();
}
else
{
var updateCards = models
.Select(model =>
{
var cachedViewModel = cache.Get(model.Id);
if (cachedViewModel != null)
{
if (!cachedViewModel.IsImporting)
{
cache.Remove(model.Id);
}
return cachedViewModel;
}
var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{
if (cache.Get(viewModel.CivitModel.Id) != null)
return;
cache.Add(viewModel.CivitModel.Id, viewModel);
};
return vm;
});
return newCard;
})
.ToList();
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed)
{
filteredCards = filteredCards.OrderByDescending(
x => x.UpdateCardText == "Update Available"
);
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
}
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
CanGoToLastPage = CurrentPageNumber != TotalPages;
// Status update
ShowMainLoadingSpinner = false;
IsIndeterminate = false;
HasSearched = true;
}
private string previousSearchQuery = string.Empty;
[RelayCommand]
private async Task SearchModels()
{
var timer = Stopwatch.StartNew();
if (SearchQuery != previousSearchQuery)
{
// Reset page number
CurrentPageNumber = 1;
previousSearchQuery = SearchQuery;
}
// Build request
var modelRequest = new CivitModelsRequest
{
Limit = MaxModelsPerPage,
Nsfw = "true", // Handled by local view filter
Sort = SortMode,
Period = SelectedPeriod,
Page = CurrentPageNumber
};
if (SearchQuery.StartsWith("#"))
{
modelRequest.Tag = SearchQuery[1..];
}
else if (SearchQuery.StartsWith("@"))
{
modelRequest.Username = SearchQuery[1..];
}
else
{
modelRequest.Query = SearchQuery;
}
if (SelectedModelType != CivitModelType.All)
{
modelRequest.Types = new[] { SelectedModelType };
}
if (SelectedBaseModelType != "All")
{
modelRequest.BaseModel = SelectedBaseModelType;
}
if (SortMode == CivitSortMode.Installed)
{
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
modelRequest.CommaSeparatedModelIds = string.Join(
",",
connectedModels
.Select(c => c.ConnectedModel!.ModelId)
.GroupBy(m => m)
.Select(g => g.First())
);
modelRequest.Sort = null;
modelRequest.Period = null;
}
else if (SortMode == CivitSortMode.Favorites)
{
var favoriteModels = settingsManager.Settings.FavoriteModels;
if (!favoriteModels.Any())
{
notificationService.Show(
"No Favorites",
"You have not added any models to your Favorites.",
NotificationType.Error
);
return;
}
modelRequest.CommaSeparatedModelIds = string.Join(",", favoriteModels);
modelRequest.Sort = null;
modelRequest.Period = null;
}
// See if query is cached
CivitModelQueryCacheEntry? cachedQuery = null;
public override string Title => "Model Browser";
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };
try
{
cachedQuery = await liteDbContext.CivitModelQueryCache
.IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
}
catch (Exception e)
{
// Suppress 'Training_Data' enum not found exceptions
// Caused by enum name change
// Ignore to do a new search to overwrite the cache
if (
!(
e is LiteException or LiteAsyncException
&& e.InnerException is ArgumentException inner
&& inner.Message.Contains("Training_Data")
)
public IReadOnlyList<TabItem> Pages { get; } =
new List<TabItem>(
new List<TabViewModelBase>([civitAiBrowserViewModel, huggingFaceViewModel]).Select(
vm => new TabItem { Header = vm.Header, Content = vm }
)
{
// Otherwise log error
Logger.Error(e, "Error while querying CivitModelQueryCache");
}
}
// 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
);
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata);
// Start remote query (background mode)
// Skip when last query was less than 2 min ago
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt;
if (timeSinceCache?.TotalMinutes >= 2)
{
CivitModelQuery(modelRequest).SafeFireAndForget();
Logger.Debug(
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query",
timeSinceCache.Value.TotalSeconds
);
}
}
else
{
// Not cached, wait for remote query
ShowMainLoadingSpinner = true;
await CivitModelQuery(modelRequest);
}
UpdateResultsText();
}
public void FirstPage()
{
CurrentPageNumber = 1;
}
public void PreviousPage()
{
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
public void LastPage()
{
CurrentPageNumber = TotalPages;
}
public void ClearSearchQuery()
{
SearchQuery = string.Empty;
}
partial void OnShowNsfwChanged(bool value)
{
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value);
// ModelCardsView?.Refresh();
var updateCards = allModelCards.Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);
if (!HasSearched)
return;
UpdateResultsText();
}
partial void OnSelectedPeriodChanged(CivitPeriod value)
{
TrySearchAgain().SafeFireAndForget();
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
)
);
}
partial void OnSelectedModelTypeChanged(CivitModelType value)
{
TrySearchAgain().SafeFireAndForget();
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
)
);
}
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)
{
if (!HasSearched)
return;
ModelCards?.Clear();
if (shouldUpdatePageNumber)
{
CurrentPageNumber = 1;
}
// execute command instead of calling method directly so that the IsRunning property gets updated
await SearchModelsCommand.ExecuteAsync(null);
}
private void UpdateResultsText()
{
NoResultsFound = ModelCards?.Count <= 0;
NoResultsText =
allModelCards.Count > 0
? $"{allModelCards.Count} results hidden by filters"
: "No results found";
}
public override string Title => "Model Browser";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };
}

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

@ -18,6 +18,7 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
@ -46,6 +47,7 @@ public partial class CheckpointFile : ViewModelBase
private string? previewImagePath;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsConnectedModel))]
private ConnectedModelInfo? connectedModel;
public bool IsConnectedModel => ConnectedModel != null;
@ -58,6 +60,9 @@ public partial class CheckpointFile : ViewModelBase
[ObservableProperty]
private CheckpointFolder parentFolder;
[ObservableProperty]
private ProgressReport? progress;
public string FileName => Path.GetFileName(FilePath);
public bool CanShowTriggerWords =>
@ -65,15 +70,8 @@ public partial class CheckpointFile : ViewModelBase
public ObservableCollection<string> Badges { get; set; } = new();
public static readonly string[] SupportedCheckpointExtensions =
{
".safetensors",
".pt",
".ckpt",
".pth",
".bin"
};
private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" };
public static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" };
private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".gif" };
private static readonly string[] SupportedMetadataExtensions = { ".json" };
partial void OnConnectedModelChanged(ConnectedModelInfo? value)
@ -97,9 +95,7 @@ public partial class CheckpointFile : ViewModelBase
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException(
"Cannot get connected model info file path when FilePath is empty"
);
throw new InvalidOperationException("Cannot get connected model info file path when FilePath is empty");
}
var modelNameNoExt = Path.GetFileNameWithoutExtension((string?)FilePath);
var modelDir = Path.GetDirectoryName((string?)FilePath) ?? "";
@ -196,10 +192,7 @@ 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"));
}
}
}
@ -231,6 +224,43 @@ public partial class CheckpointFile : ViewModelBase
return App.Clipboard.SetTextAsync(words);
}
[RelayCommand]
private async Task FindConnectedMetadata(bool forceReimport = false)
{
if (App.Services.GetService(typeof(IMetadataImportService)) is not IMetadataImportService importService)
return;
IsLoading = true;
try
{
var progressReport = new Progress<ProgressReport>(report =>
{
Progress = report;
});
var cmInfo = await importService.GetMetadataForFile(FilePath, progressReport, forceReimport);
if (cmInfo != null)
{
ConnectedModel = cmInfo;
PreviewImagePath = SupportedImageExtensions
.Select(
ext =>
Path.Combine(
ParentFolder.DirectoryPath,
$"{Path.GetFileNameWithoutExtension(FileName)}.preview{ext}"
)
)
.Where(File.Exists)
.FirstOrDefault();
}
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Indexes directory and yields all checkpoint files.
/// First we match all files with supported extensions.
@ -248,9 +278,7 @@ public partial class CheckpointFile : ViewModelBase
{
if (
!SupportedCheckpointExtensions.Any(
ext =>
Path.GetExtension(file)
.Equals(ext, StringComparison.InvariantCultureIgnoreCase)
ext => Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase)
)
)
continue;
@ -261,10 +289,7 @@ public partial class CheckpointFile : ViewModelBase
FilePath = Path.Combine(directory, file),
};
var jsonPath = Path.Combine(
directory,
$"{Path.GetFileNameWithoutExtension(file)}.cm-info.json"
);
var jsonPath = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.cm-info.json");
if (File.Exists(jsonPath))
{
var json = File.ReadAllText(jsonPath);
@ -273,13 +298,7 @@ public partial class CheckpointFile : ViewModelBase
}
checkpointFile.PreviewImagePath = SupportedImageExtensions
.Select(
ext =>
Path.Combine(
directory,
$"{Path.GetFileNameWithoutExtension(file)}.preview{ext}"
)
)
.Select(ext => Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}"))
.Where(File.Exists)
.FirstOrDefault();
@ -296,28 +315,16 @@ public partial class CheckpointFile : ViewModelBase
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)
ext => Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase)
)
)
continue;
var checkpointFile = new CheckpointFile
{
Title = Path.GetFileNameWithoutExtension(file),
FilePath = file,
};
var checkpointFile = new CheckpointFile { Title = Path.GetFileNameWithoutExtension(file), FilePath = file };
var jsonPath = Path.Combine(
Path.GetDirectoryName(file) ?? "",
@ -425,6 +432,5 @@ public partial class CheckpointFile : ViewModelBase
}
}
public static IEqualityComparer<CheckpointFile> FilePathComparer { get; } =
new FilePathEqualityComparer();
public static IEqualityComparer<CheckpointFile> FilePathComparer { get; } = new FilePathEqualityComparer();
}

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

@ -37,12 +37,11 @@ public partial class CheckpointFolder : ViewModelBase
private readonly IDownloadService downloadService;
private readonly ModelFinder modelFinder;
private readonly INotificationService notificationService;
private readonly IMetadataImportService metadataImportService;
public SourceCache<CheckpointFolder, string> SubFoldersCache { get; } =
new(x => x.DirectoryPath);
public SourceCache<CheckpointFolder, string> SubFoldersCache { get; } = new(x => x.DirectoryPath);
private readonly SourceCache<CheckpointFile, string> checkpointFilesCache =
new(x => x.FilePath);
private readonly SourceCache<CheckpointFile, string> checkpointFilesCache = new(x => x.FilePath);
// ReSharper disable once FieldCanBeMadeReadOnly.Local
private bool useCategoryVisibility;
@ -111,6 +110,7 @@ public partial class CheckpointFolder : ViewModelBase
IDownloadService downloadService,
ModelFinder modelFinder,
INotificationService notificationService,
IMetadataImportService metadataImportService,
bool useCategoryVisibility = true
)
{
@ -118,6 +118,7 @@ public partial class CheckpointFolder : ViewModelBase
this.downloadService = downloadService;
this.modelFinder = modelFinder;
this.notificationService = notificationService;
this.metadataImportService = metadataImportService;
this.useCategoryVisibility = useCategoryVisibility;
checkpointFilesCache
@ -133,9 +134,7 @@ public partial class CheckpointFolder : ViewModelBase
.Sort(
SortExpressionComparer<CheckpointFile>
.Descending(f => f.IsConnectedModel)
.ThenByAscending(
f => f.IsConnectedModel ? f.ConnectedModel!.ModelName : f.FileName
)
.ThenByAscending(f => f.IsConnectedModel ? f.ConnectedModel!.ModelName : f.FileName)
)
.Filter(
f =>
@ -148,6 +147,12 @@ public partial class CheckpointFolder : ViewModelBase
SubFoldersCache
.Connect()
.DeferUntilLoaded()
.SubscribeMany(
folder =>
Observable
.FromEventPattern<EventArgs>(folder, nameof(ParentListRemoveRequested))
.Subscribe(_ => SubFoldersCache.Remove(folder))
)
.SortBy(x => x.Title)
.Bind(SubFolders)
.Subscribe();
@ -242,17 +247,10 @@ public partial class CheckpointFolder : ViewModelBase
return;
}
var dialog = DialogHelper.CreateTaskDialog(
"Are you sure you want to delete this folder?",
directory
);
var dialog = DialogHelper.CreateTaskDialog("Are you sure you want to delete this folder?", directory);
dialog.ShowProgressBar = false;
dialog.Buttons = new List<TaskDialogButton>
{
TaskDialogButton.YesButton,
TaskDialogButton.NoButton
};
dialog.Buttons = new List<TaskDialogButton> { TaskDialogButton.YesButton, TaskDialogButton.NoButton };
dialog.Closing += async (sender, e) =>
{
@ -306,12 +304,13 @@ public partial class CheckpointFolder : ViewModelBase
Directory.CreateDirectory(subFolderPath);
SubFolders.Add(
SubFoldersCache.AddOrUpdate(
new CheckpointFolder(
settingsManager,
downloadService,
modelFinder,
notificationService,
metadataImportService,
useCategoryVisibility: false
)
{
@ -333,23 +332,11 @@ public partial class CheckpointFolder : ViewModelBase
Progress.Value = 0;
var sourcePath = new FilePath(sourceFile.FilePath);
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(sourcePath);
var sourceCmInfoPath = Path.Combine(
sourcePath.Directory,
$"{fileNameWithoutExt}.cm-info.json"
);
var sourcePreviewPath = Path.Combine(
sourcePath.Directory,
$"{fileNameWithoutExt}.preview.jpeg"
);
var sourceCmInfoPath = Path.Combine(sourcePath.Directory, $"{fileNameWithoutExt}.cm-info.json");
var sourcePreviewPath = Path.Combine(sourcePath.Directory, $"{fileNameWithoutExt}.preview.jpeg");
var destinationFilePath = Path.Combine(DirectoryPath, sourcePath.Name);
var destinationCmInfoPath = Path.Combine(
DirectoryPath,
$"{fileNameWithoutExt}.cm-info.json"
);
var destinationPreviewPath = Path.Combine(
DirectoryPath,
$"{fileNameWithoutExt}.preview.jpeg"
);
var destinationCmInfoPath = Path.Combine(DirectoryPath, $"{fileNameWithoutExt}.cm-info.json");
var destinationPreviewPath = Path.Combine(DirectoryPath, $"{fileNameWithoutExt}.preview.jpeg");
// Move files
if (File.Exists(sourcePath))
@ -393,19 +380,12 @@ public partial class CheckpointFolder : ViewModelBase
/// <summary>
/// Imports files to the folder. Reports progress to instance properties.
/// </summary>
public async Task ImportFilesAsync(
IEnumerable<string> files,
bool convertToConnected = false,
bool copyFiles = true
)
public async Task ImportFilesAsync(IEnumerable<string> files, bool convertToConnected = false)
{
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 =>
{
@ -413,15 +393,10 @@ public partial class CheckpointFolder : ViewModelBase
Progress.Value = report.Percentage;
// For multiple files, add count
Progress.Text =
copyPaths.Count > 1
? $"Importing {report.Title} ({report.Message})"
: $"Importing {report.Title}";
copyPaths.Count > 1 ? $"Importing {report.Title} ({report.Message})" : $"Importing {report.Title}";
});
if (copyFiles)
{
await FileTransfers.CopyFiles(copyPaths, progress);
}
await FileTransfers.CopyFiles(copyPaths, progress);
// Hash files and convert them to connected model if found
if (convertToConnected)
@ -459,12 +434,7 @@ public partial class CheckpointFolder : ViewModelBase
// Save connected model info json
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name);
var modelInfo = new ConnectedModelInfo(
model,
version,
file,
DateTimeOffset.UtcNow
);
var modelInfo = new ConnectedModelInfo(model, version, file, DateTimeOffset.UtcNow);
await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName);
// If available, save thumbnail
@ -475,15 +445,9 @@ 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);
}
}
@ -531,22 +495,34 @@ public partial class CheckpointFolder : ViewModelBase
public async Task FindConnectedMetadata()
{
IsImportInProgress = true;
var files = CheckpointFiles
.Where(f => !f.IsConnectedModel)
.Select(f => f.FilePath)
.ToList();
var files = CheckpointFiles.Where(f => !f.IsConnectedModel).Select(f => f.FilePath).ToList();
if (files.Any())
try
{
await ImportFilesAsync(files, true, false);
if (files.Count > 0)
{
var progress = new Progress<ProgressReport>(report =>
{
Progress.IsIndeterminate = report.IsIndeterminate;
Progress.Value = report.Percentage;
Progress.Text = report.Title;
});
await metadataImportService.ScanDirectoryForMissingInfo(DirectoryPath, progress);
BackgroundIndex();
notificationService.Show("Scan Complete", "Finished scanning for missing metadata");
}
else
{
notificationService.Show(
"Cannot Find Connected Metadata",
"All files in this folder are already connected.",
NotificationType.Warning
);
}
}
else
finally
{
notificationService.Show(
"Cannot Find Connected Metadata",
"All files in this folder are already connected.",
NotificationType.Warning
);
DelayedClearProgress(TimeSpan.FromSeconds(1.5));
}
}
@ -591,6 +567,7 @@ public partial class CheckpointFolder : ViewModelBase
/// </summary>
public void Index()
{
var updatedFolders = new List<CheckpointFolder>();
// Get subfolders
foreach (var folder in Directory.GetDirectories(DirectoryPath))
{
@ -598,6 +575,7 @@ public partial class CheckpointFolder : ViewModelBase
if (SubFoldersCache.Lookup(folder) is { HasValue: true } result)
{
result.Value.BackgroundIndex();
updatedFolders.Add(result.Value);
}
else
{
@ -607,6 +585,7 @@ public partial class CheckpointFolder : ViewModelBase
downloadService,
modelFinder,
notificationService,
metadataImportService,
useCategoryVisibility: false
)
{
@ -617,18 +596,22 @@ public partial class CheckpointFolder : ViewModelBase
IsExpanded = false, // Subfolders are collapsed by default
};
subFolder.BackgroundIndex();
SubFoldersCache.AddOrUpdate(subFolder);
updatedFolders.Add(subFolder);
}
}
SubFoldersCache.EditDiff(updatedFolders, (a, b) => a.Title == b.Title);
// Index files
Dispatcher.UIThread.Post(
() =>
{
var files = GetCheckpointFiles();
checkpointFilesCache.EditDiff(files, CheckpointFile.FilePathComparer);
},
DispatcherPriority.Background
);
Dispatcher
.UIThread
.Post(
() =>
{
var files = GetCheckpointFiles();
checkpointFilesCache.EditDiff(files, CheckpointFile.FilePathComparer);
},
DispatcherPriority.Background
);
}
}

86
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
@ -16,6 +18,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
@ -34,11 +37,11 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
private readonly ModelFinder modelFinder;
private readonly IDownloadService downloadService;
private readonly INotificationService notificationService;
private readonly IMetadataImportService metadataImportService;
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]
@ -59,18 +62,18 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
[ObservableProperty]
private bool isCategoryTipOpen;
[ObservableProperty]
private ProgressReport progress;
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);
}
}
public SourceCache<CheckpointFolder, string> CheckpointFoldersCache { get; } =
new(x => x.DirectoryPath);
public SourceCache<CheckpointFolder, string> CheckpointFoldersCache { get; } = new(x => x.DirectoryPath);
public IObservableCollection<CheckpointFolder> CheckpointFolders { get; } =
new ObservableCollectionExtended<CheckpointFolder>();
@ -83,6 +86,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
ISettingsManager settingsManager,
IDownloadService downloadService,
INotificationService notificationService,
IMetadataImportService metadataImportService,
ModelFinder modelFinder
)
{
@ -90,6 +94,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
this.settingsManager = settingsManager;
this.downloadService = downloadService;
this.notificationService = notificationService;
this.metadataImportService = metadataImportService;
this.modelFinder = modelFinder;
CheckpointFoldersCache
@ -118,14 +123,10 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
if (Design.IsDesignMode)
return;
if (
!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.CheckpointCategoriesTip)
)
if (!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.CheckpointCategoriesTip))
{
IsCategoryTipOpen = true;
settingsManager.Transaction(
s => s.SeenTeachingTips.Add(TeachingTip.CheckpointCategoriesTip)
);
settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.CheckpointCategoriesTip));
}
IsLoading = CheckpointFolders.Count == 0;
@ -156,10 +157,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
partial void OnShowConnectedModelImagesChanged(bool value)
{
if (
settingsManager.IsLibraryDirSet
&& value != settingsManager.Settings.ShowConnectedModelImages
)
if (settingsManager.IsLibraryDirSet && value != settingsManager.Settings.ShowConnectedModelImages)
{
settingsManager.Transaction(s => s.ShowConnectedModelImages = value);
}
@ -176,9 +174,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
}
// Check files in the current folder
return folder.CheckpointFiles.Any(
x => x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)
)
return folder.CheckpointFiles.Any(x => x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase))
||
// If no matching files were found in the current folder, check in all subfolders
folder.SubFolders.Any(ContainsSearchFilter);
@ -195,14 +191,16 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
var sw = Stopwatch.StartNew();
// Index all folders
var updatedFolders = new List<CheckpointFolder>();
// Index all folders
foreach (var folder in folders)
{
// Get from cache or create new
if (CheckpointFoldersCache.Lookup(folder) is { HasValue: true } result)
{
result.Value.Index();
updatedFolders.Add(result.Value);
}
else
{
@ -210,7 +208,8 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
settingsManager,
downloadService,
modelFinder,
notificationService
notificationService,
metadataImportService
)
{
Title = Path.GetFileName(folder),
@ -218,10 +217,12 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
IsExpanded = true // Top level folders expanded by default
};
checkpointFolder.Index();
CheckpointFoldersCache.AddOrUpdate(checkpointFolder);
updatedFolders.Add(checkpointFolder);
}
}
CheckpointFoldersCache.EditDiff(updatedFolders, (a, b) => a.Title == b.Title);
sw.Stop();
Logger.Info($"IndexFolders in {sw.Elapsed.TotalMilliseconds:F1}ms");
}
@ -231,4 +232,43 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
{
await ProcessRunner.OpenFolderBrowser(settingsManager.ModelsDirectory);
}
[RelayCommand]
private async Task FindConnectedMetadata()
{
var progressHandler = new Progress<ProgressReport>(report =>
{
Progress = report;
});
await metadataImportService.ScanDirectoryForMissingInfo(settingsManager.ModelsDirectory, progressHandler);
notificationService.Show("Scan Complete", "Finished scanning for missing metadata.", NotificationType.Success);
DelayedClearProgress(TimeSpan.FromSeconds(1.5));
}
[RelayCommand]
private async Task UpdateExistingMetadata()
{
var progressHandler = new Progress<ProgressReport>(report =>
{
Progress = report;
});
await metadataImportService.UpdateExistingMetadata(settingsManager.ModelsDirectory, progressHandler);
notificationService.Show("Scan Complete", "Finished updating metadata.", NotificationType.Success);
DelayedClearProgress(TimeSpan.FromSeconds(1.5));
}
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay)
.ContinueWith(_ =>
{
IsLoading = false;
Progress = new ProgressReport(0, 0);
});
}
}

45
StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs

@ -54,8 +54,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
/// <summary>
/// Gets a cancellation token using the cursor lock timeout
/// </summary>
private CancellationToken WriteCursorLockTimeoutToken =>
new CancellationTokenSource(WriteCursorLockTimeout).Token;
private CancellationToken WriteCursorLockTimeoutToken => new CancellationTokenSource(WriteCursorLockTimeout).Token;
/// <summary>
/// Event invoked when an ApcMessage of type Input is received.
@ -132,9 +131,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{
using (await writeCursorLock.LockAsync(WriteCursorLockTimeoutToken))
{
Logger.ConditionalTrace(
$"Reset cursor to end: ({writeCursor} -> {Document.TextLength})"
);
Logger.ConditionalTrace($"Reset cursor to end: ({writeCursor} -> {Document.TextLength})");
writeCursor = Document.TextLength;
}
DebugPrintDocument();
@ -146,9 +143,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
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
{
@ -176,9 +171,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
);
// Link the cancellation token to the write cursor lock timeout
var linkedCt = CancellationTokenSource
.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken)
.Token;
var linkedCt = CancellationTokenSource.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken).Token;
using (await writeCursorLock.LockAsync(linkedCt))
{
@ -193,10 +186,7 @@ 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}");
}
}
@ -248,8 +238,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
Document.Remove(lineStartOffset, lineLength);
Logger.ConditionalTrace(
$"Moving cursor to start for carriage return "
+ $"({writeCursor} -> {lineStartOffset})"
$"Moving cursor to start for carriage return " + $"({writeCursor} -> {lineStartOffset})"
);
writeCursor = lineStartOffset;
}
@ -275,10 +264,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
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
@ -297,13 +283,17 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{
// Get the current line, we'll insert spaces from start to end
var currentLine = Document.GetLineByOffset(writeCursor);
// Must be smaller than total lines
currentLine =
currentLine.LineNumber < Document.LineCount
? currentLine
: Document.GetLineByNumber(Document.LineCount - 1);
// Make some spaces to insert
var spaces = new string(' ', currentLine.Length);
// Insert the text
Logger.ConditionalTrace(
$"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);
@ -346,8 +336,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{
var nextLine = Document.GetLineByNumber(currentLine.LineNumber + 1);
Logger.ConditionalTrace(
$"Moving cursor to start of next line "
+ $"({writeCursor} -> {nextLine.Offset})"
$"Moving cursor to start of next line " + $"({writeCursor} -> {nextLine.Offset})"
);
writeCursor = nextLine.Offset;
}
@ -393,9 +382,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (remainingLength > 0)
{
var textToInsert = text[replaceLength..];
Logger.ConditionalTrace(
$"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})"
);
Logger.ConditionalTrace($"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})");
Document.Insert(writeCursor, textToInsert);
writeCursor += textToInsert.Length;

47
StabilityMatrix.Avalonia/ViewModels/Dialogs/PropertyGridViewModel.cs

@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia;
using Avalonia.PropertyGrid.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using OneOf;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(PropertyGridDialog))]
[ManagedService]
[Transient]
public partial class PropertyGridViewModel : ContentDialogViewModelBase
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedObjectItemsSource))]
private OneOf<INotifyPropertyChanged, IEnumerable<INotifyPropertyChanged>>? selectedObject;
public IEnumerable<INotifyPropertyChanged>? SelectedObjectItemsSource =>
SelectedObject?.Match(single => [single], multiple => multiple);
[ObservableProperty]
private PropertyGridShowStyle showStyle = PropertyGridShowStyle.Alphabetic;
[ObservableProperty]
private IReadOnlyList<string>? excludeCategories;
[ObservableProperty]
private IReadOnlyList<string>? includeCategories;
/// <inheritdoc />
public override BetterContentDialog GetDialog()
{
var dialog = base.GetDialog();
dialog.Padding = new Thickness(0);
dialog.CloseOnClickOutside = true;
dialog.CloseButtonText = Resources.Action_Close;
return dialog;
}
}

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

@ -189,32 +189,41 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
App.Shutdown();
}
internal async Task<string> GetReleaseNotes(string changelogUrl)
{
using var client = httpClientFactory.CreateClient();
var response = await client.GetAsync(changelogUrl);
if (response.IsSuccessStatusCode)
{
var changelog = await response.Content.ReadAsStringAsync();
// Formatting for new changelog format
// https://keepachangelog.com/en/1.1.0/
if (changelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
try
{
var response = await client.GetAsync(changelogUrl);
if (response.IsSuccessStatusCode)
{
return FormatChangelog(
changelog,
Compat.AppVersion,
settingsManager.Settings.PreferredUpdateChannel
) ?? "## Unable to format release notes";
var changelog = await response.Content.ReadAsStringAsync();
// Formatting for new changelog format
// https://keepachangelog.com/en/1.1.0/
if (changelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
{
return FormatChangelog(
changelog,
Compat.AppVersion,
settingsManager.Settings.PreferredUpdateChannel
) ?? "## Unable to format release notes";
}
return changelog;
}
return changelog;
return "## Unable to load release notes";
}
else
catch (HttpRequestException e)
{
return "## Unable to load release notes";
return $"## Unable to fetch release notes ({e.StatusCode})\n\n[{changelogUrl}]({changelogUrl})";
}
catch (TaskCanceledException) { }
return $"## Unable to fetch release notes\n\n[{changelogUrl}]({changelogUrl})";
}
/// <summary>

8
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

@ -7,6 +7,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
namespace StabilityMatrix.Avalonia.ViewModels;
@ -25,8 +26,7 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
private RefreshBadgeViewModel checkHardwareBadge =
new()
{
WorkingToolTipText =
"We're checking some hardware specifications to determine compatibility.",
WorkingToolTipText = "We're checking some hardware specifications to determine compatibility.",
SuccessToolTipText = "Everything looks good!",
FailToolTipText =
"We recommend a GPU with CUDA support for the best experience. "
@ -48,9 +48,7 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
gpuInfo = await Task.Run(() => HardwareHelper.IterGpuInfo().ToArray());
}
// First Nvidia GPU
var activeGpu = gpuInfo.FirstOrDefault(
gpu => gpu.Name?.ToLowerInvariant().Contains("nvidia") ?? false
);
var activeGpu = gpuInfo.FirstOrDefault(gpu => gpu.Name?.ToLowerInvariant().Contains("nvidia") ?? false);
var isNvidia = activeGpu is not null;
// Otherwise first GPU
activeGpu ??= gpuInfo.FirstOrDefault();

52
StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/CategoryViewModel.cs

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using StabilityMatrix.Avalonia.Models.HuggingFace;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.ViewModels.HuggingFacePage;
public partial class CategoryViewModel : ViewModelBase
{
[ObservableProperty]
private IObservableCollection<HuggingfaceItemViewModel> items =
new ObservableCollectionExtended<HuggingfaceItemViewModel>();
public SourceCache<HuggingfaceItem, string> ItemsCache { get; } = new(i => i.RepositoryPath + i.ModelName);
[ObservableProperty]
private string? title;
[ObservableProperty]
private bool isChecked;
[ObservableProperty]
private int numSelected;
public CategoryViewModel(IEnumerable<HuggingfaceItem> items)
{
ItemsCache
.Connect()
.DeferUntilLoaded()
.Transform(i => new HuggingfaceItemViewModel { Item = i })
.Bind(Items)
.WhenPropertyChanged(p => p.IsSelected)
.Subscribe(_ => NumSelected = Items.Count(i => i.IsSelected));
ItemsCache.EditDiff(items, (a, b) => a.RepositoryPath == b.RepositoryPath);
}
partial void OnIsCheckedChanged(bool value)
{
if (Items is null)
return;
foreach (var item in Items)
{
item.IsSelected = value;
}
}
}

25
StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/HuggingfaceItemViewModel.cs

@ -0,0 +1,25 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Models.HuggingFace;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.ViewModels.HuggingFacePage;
public partial class HuggingfaceItemViewModel : ViewModelBase
{
[ObservableProperty]
private HuggingfaceItem item;
[ObservableProperty]
private bool isSelected;
public string LicenseUrl =>
$"https://huggingface.co/{Item.RepositoryPath}/blob/main/{Item.LicensePath ?? "README.md"}";
public string RepoUrl => $"https://huggingface.co/{Item.RepositoryPath}";
[RelayCommand]
private void ToggleSelected()
{
IsSelected = !IsSelected;
}
}

42
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using StabilityMatrix.Avalonia.Controls;
@ -23,7 +24,7 @@ public class ControlNetModule : ModuleBase
AddCards(vmFactory.Get<ControlNetCardViewModel>());
}
public IEnumerable<ImageSource> GetInputImages()
protected override IEnumerable<ImageSource> GetInputImages()
{
if (GetCard<ControlNetCardViewModel>().SelectImageCardViewModel.ImageSource is { } image)
{
@ -43,7 +44,7 @@ public class ControlNetModule : ModuleBase
Image =
card.SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached(
"Inference"
) ?? throw new ValidationException()
) ?? throw new ValidationException("No ImageSource")
}
);
@ -51,18 +52,22 @@ public class ControlNetModule : ModuleBase
new ComfyNodeBuilder.ControlNetLoader
{
Name = e.Nodes.GetUniqueName("ControlNetLoader"),
ControlNetName = card.SelectedModel?.FileName ?? throw new ValidationException(),
ControlNetName =
card.SelectedModel?.FileName
?? throw new ValidationException("No SelectedModel"),
}
);
var controlNetApply = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ControlNetApplyAdvanced
{
Name = e.Nodes.GetUniqueName("ControlNet"),
Name = e.Nodes.GetUniqueName("ControlNetApply"),
Image = imageLoad.Output1,
ControlNet = controlNetLoader.Output,
Positive = e.Temp.Conditioning.Positive,
Negative = e.Temp.Conditioning.Negative,
Positive =
e.Temp.Conditioning?.Positive ?? throw new ArgumentException("No Conditioning"),
Negative =
e.Temp.Conditioning?.Negative ?? throw new ArgumentException("No Conditioning"),
Strength = card.Strength,
StartPercent = card.StartPercent,
EndPercent = card.EndPercent,
@ -70,5 +75,28 @@ public class ControlNetModule : ModuleBase
);
e.Temp.Conditioning = (controlNetApply.Output1, controlNetApply.Output2);
// Refiner if available
if (e.Temp.RefinerConditioning is not null)
{
var controlNetRefinerApply = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.ControlNetApplyAdvanced
{
Name = e.Nodes.GetUniqueName("Refiner_ControlNetApply"),
Image = imageLoad.Output1,
ControlNet = controlNetLoader.Output,
Positive = e.Temp.RefinerConditioning.Value.Positive,
Negative = e.Temp.RefinerConditioning.Value.Negative,
Strength = card.Strength,
StartPercent = card.StartPercent,
EndPercent = card.EndPercent,
}
);
e.Temp.RefinerConditioning = (
controlNetRefinerApply.Output1,
controlNetRefinerApply.Output2
);
}
}
}

76
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs

@ -1,8 +1,15 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models.Api.Comfy;
@ -12,8 +19,14 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
[ManagedService]
[Transient]
public class HiresFixModule : ModuleBase
public partial class HiresFixModule : ModuleBase
{
/// <inheritdoc />
public override bool IsSettingsEnabled => true;
/// <inheritdoc />
public override IRelayCommand SettingsCommand => OpenSettingsDialogCommand;
/// <inheritdoc />
public HiresFixModule(ServiceManager<ViewModelBase> vmFactory)
: base(vmFactory)
@ -28,6 +41,19 @@ public class HiresFixModule : ModuleBase
);
}
[RelayCommand]
private async Task OpenSettingsDialog()
{
var gridVm = VmFactory.Get<PropertyGridViewModel>(vm =>
{
vm.Title = $"{Title} {Resources.Label_Settings}";
vm.SelectedObject = Cards.ToArray();
vm.IncludeCategories = ["Settings"];
});
await gridVm.GetDialog().ShowAsync();
}
/// <inheritdoc />
protected override void OnApplyStep(ModuleApplyStepEventArgs e)
{
@ -55,28 +81,30 @@ public class HiresFixModule : ModuleBase
);
}
var hiresSampler = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"),
Model = builder.Connections.GetRefinerOrBaseModel(),
Seed = builder.Connections.Seed,
Steps = samplerCard.Steps,
Cfg = samplerCard.CfgScale,
SamplerName =
samplerCard.SelectedSampler?.Name
?? e.Builder.Connections.PrimarySampler?.Name
?? throw new ArgumentException("No PrimarySampler"),
Scheduler =
samplerCard.SelectedScheduler?.Name
?? e.Builder.Connections.PrimaryScheduler?.Name
?? throw new ArgumentException("No PrimaryScheduler"),
Positive = builder.Connections.GetRefinerOrBaseConditioning(),
Negative = builder.Connections.GetRefinerOrBaseNegativeConditioning(),
LatentImage = builder.GetPrimaryAsLatent(),
Denoise = samplerCard.DenoiseStrength
}
);
var hiresSampler = builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"),
Model = builder.Connections.GetRefinerOrBaseModel(),
Seed = builder.Connections.Seed,
Steps = samplerCard.Steps,
Cfg = samplerCard.CfgScale,
SamplerName =
samplerCard.SelectedSampler?.Name
?? e.Builder.Connections.PrimarySampler?.Name
?? throw new ArgumentException("No PrimarySampler"),
Scheduler =
samplerCard.SelectedScheduler?.Name
?? e.Builder.Connections.PrimaryScheduler?.Name
?? throw new ArgumentException("No PrimaryScheduler"),
Positive = builder.Connections.GetRefinerOrBaseConditioning(),
Negative = builder.Connections.GetRefinerOrBaseNegativeConditioning(),
LatentImage = builder.GetPrimaryAsLatent(),
Denoise = samplerCard.DenoiseStrength
}
);
// Set as primary
builder.Connections.Primary = hiresSampler.Output;

24
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ModuleBase.cs

@ -8,19 +8,29 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
public abstract class ModuleBase : StackExpanderViewModel, IComfyStep, IInputImageProvider
{
protected readonly ServiceManager<ViewModelBase> VmFactory;
/// <inheritdoc />
protected ModuleBase(ServiceManager<ViewModelBase> vmFactory)
: base(vmFactory) { }
: base(vmFactory)
{
VmFactory = vmFactory;
}
/// <inheritdoc />
public void ApplyStep(ModuleApplyStepEventArgs e)
{
if (
(
e.IsEnabledOverrides.TryGetValue(GetType(), out var isEnabledOverride)
&& !isEnabledOverride
) || !IsEnabled
)
if (e.IsEnabledOverrides.TryGetValue(GetType(), out var isEnabledOverride))
{
if (isEnabledOverride)
{
OnApplyStep(e);
}
return;
}
if (!IsEnabled)
{
return;
}

21
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/SaveImageModule.cs

@ -1,4 +1,5 @@
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
@ -14,19 +15,21 @@ public class SaveImageModule : ModuleBase
public SaveImageModule(ServiceManager<ViewModelBase> vmFactory)
: base(vmFactory)
{
Title = "Save Intermediary Image";
Title = Resources.Label_SaveIntermediateImage;
}
/// <inheritdoc />
protected override void OnApplyStep(ModuleApplyStepEventArgs e)
{
var preview = e.Builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.PreviewImage
{
Name = e.Builder.Nodes.GetUniqueName("SaveIntermediaryImage"),
Images = e.Builder.GetPrimaryAsImage()
}
);
var preview = e.Builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.PreviewImage
{
Name = e.Builder.Nodes.GetUniqueName("SaveIntermediateImage"),
Images = e.Builder.GetPrimaryAsImage()
}
);
e.Builder.Connections.OutputNodes.Add(preview);
}

61
StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs

@ -1,10 +1,12 @@
using System;
using System.Collections.Immutable;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
@ -15,16 +17,14 @@ using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(SamplerCard))]
[ManagedService]
[Transient]
public partial class SamplerCardViewModel
: LoadableViewModelBase,
IParametersLoadableState,
IComfyStep
public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLoadableState, IComfyStep
{
public const string ModuleKey = "Sampler";
@ -44,6 +44,7 @@ public partial class SamplerCardViewModel
private double denoiseStrength = 1;
[ObservableProperty]
[property: Category("Settings")]
private bool isCfgScaleEnabled;
[ObservableProperty]
@ -59,6 +60,7 @@ public partial class SamplerCardViewModel
private int height = 512;
[ObservableProperty]
[property: Category("Settings")]
private bool isSamplerSelectionEnabled;
[ObservableProperty]
@ -66,6 +68,7 @@ public partial class SamplerCardViewModel
private ComfySampler? selectedSampler = ComfySampler.EulerAncestral;
[ObservableProperty]
[property: Category("Settings")]
private bool isSchedulerSelectionEnabled;
[ObservableProperty]
@ -80,17 +83,13 @@ public partial class SamplerCardViewModel
private int TotalSteps => Steps + RefinerSteps;
public SamplerCardViewModel(
IInferenceClientManager clientManager,
ServiceManager<ViewModelBase> vmFactory
)
public SamplerCardViewModel(IInferenceClientManager clientManager, ServiceManager<ViewModelBase> vmFactory)
{
ClientManager = clientManager;
ModulesCardViewModel = vmFactory.Get<StackEditableCardViewModel>(modulesCard =>
{
modulesCard.Title = "Addons";
modulesCard.Title = Resources.Label_Addons;
modulesCard.AvailableModules = new[] { typeof(FreeUModule), typeof(ControlNetModule) };
modulesCard.InitializeDefaults();
});
ModulesCardViewModel.CardAdded += (
@ -146,8 +145,7 @@ public partial class SamplerCardViewModel
var primaryLatent = e.Builder.GetPrimaryAsLatent(vae);
// Set primary sampler and scheduler
e.Builder.Connections.PrimarySampler =
SelectedSampler ?? throw new ValidationException("Sampler not selected");
e.Builder.Connections.PrimarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected");
e.Builder.Connections.PrimaryScheduler =
SelectedScheduler ?? throw new ValidationException("Scheduler not selected");
@ -159,16 +157,14 @@ public partial class SamplerCardViewModel
new ComfyNodeBuilder.KSampler
{
Name = "Sampler",
Model =
e.Builder.Connections.BaseModel
?? throw new ArgumentException("No BaseModel"),
Model = e.Builder.Connections.BaseModel ?? throw new ArgumentException("No BaseModel"),
Seed = e.Builder.Connections.Seed,
SamplerName = e.Builder.Connections.PrimarySampler?.Name!,
Scheduler = e.Builder.Connections.PrimaryScheduler?.Name!,
Steps = Steps,
Cfg = CfgScale,
Positive = e.Temp.Conditioning.Positive,
Negative = e.Temp.Conditioning.Negative,
Positive = e.Temp.Conditioning?.Positive!,
Negative = e.Temp.Conditioning?.Negative!,
LatentImage = primaryLatent,
Denoise = DenoiseStrength,
}
@ -183,17 +179,15 @@ public partial class SamplerCardViewModel
new ComfyNodeBuilder.KSamplerAdvanced
{
Name = "Sampler",
Model =
e.Builder.Connections.BaseModel
?? throw new ArgumentException("No BaseModel"),
Model = e.Builder.Connections.BaseModel ?? throw new ArgumentException("No BaseModel"),
AddNoise = true,
NoiseSeed = e.Builder.Connections.Seed,
Steps = TotalSteps,
Cfg = CfgScale,
SamplerName = e.Builder.Connections.PrimarySampler?.Name!,
Scheduler = e.Builder.Connections.PrimaryScheduler?.Name!,
Positive = e.Temp.Conditioning.Positive,
Negative = e.Temp.Conditioning.Negative,
Positive = e.Temp.Conditioning?.Positive!,
Negative = e.Temp.Conditioning?.Negative!,
LatentImage = primaryLatent,
StartAtStep = 0,
EndAtStep = Steps,
@ -206,17 +200,15 @@ public partial class SamplerCardViewModel
new ComfyNodeBuilder.KSamplerAdvanced
{
Name = "Refiner_Sampler",
Model =
e.Builder.Connections.RefinerModel
?? throw new ArgumentException("No RefinerModel"),
Model = e.Builder.Connections.RefinerModel ?? throw new ArgumentException("No RefinerModel"),
AddNoise = false,
NoiseSeed = e.Builder.Connections.Seed,
Steps = TotalSteps,
Cfg = CfgScale,
SamplerName = e.Builder.Connections.PrimarySampler?.Name!,
Scheduler = e.Builder.Connections.PrimaryScheduler?.Name!,
Positive = e.Temp.RefinerConditioning.Positive,
Negative = e.Temp.RefinerConditioning.Negative,
Positive = e.Temp.RefinerConditioning?.Positive!,
Negative = e.Temp.RefinerConditioning?.Negative!,
// Connect to previous sampler
LatentImage = sampler.Output,
StartAtStep = Steps,
@ -254,18 +246,11 @@ public partial class SamplerCardViewModel
if (
!string.IsNullOrEmpty(parameters.Sampler)
&& GenerationParametersConverter.TryGetSamplerScheduler(
parameters.Sampler,
out var samplerScheduler
)
&& GenerationParametersConverter.TryGetSamplerScheduler(parameters.Sampler, out var samplerScheduler)
)
{
SelectedSampler = ClientManager.Samplers.FirstOrDefault(
s => s == samplerScheduler.Sampler
);
SelectedScheduler = ClientManager.Schedulers.FirstOrDefault(
s => s == samplerScheduler.Scheduler
);
SelectedSampler = ClientManager.Samplers.FirstOrDefault(s => s == samplerScheduler.Sampler);
SelectedScheduler = ClientManager.Schedulers.FirstOrDefault(s => s == samplerScheduler.Scheduler);
}
}

4
StabilityMatrix.Avalonia/ViewModels/Inference/StackEditableCardViewModel.cs

@ -62,7 +62,7 @@ public partial class StackEditableCardViewModel : StackViewModelBase
partial void OnIsEditEnabledChanged(bool value)
{
// Propagate edit state to children
foreach (var module in Cards.OfType<ModuleBase>())
foreach (var module in Cards.OfType<StackExpanderViewModel>())
{
module.IsEditEnabled = value;
}
@ -73,7 +73,7 @@ public partial class StackEditableCardViewModel : StackViewModelBase
{
base.OnCardAdded(item);
if (item is ModuleBase module)
if (item is StackExpanderViewModel module)
{
// Inherit our edit state
module.IsEditEnabled = IsEditEnabled;

12
StabilityMatrix.Avalonia/ViewModels/Inference/StackExpanderViewModel.cs

@ -1,6 +1,7 @@
using System.Linq;
using System.Text.Json.Nodes;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Newtonsoft.Json;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models.Inference;
@ -27,6 +28,9 @@ public partial class StackExpanderViewModel : StackViewModelBase
[property: JsonIgnore]
private string? titleExtra;
[ObservableProperty]
private bool isEnabled;
/// <summary>
/// True if parent StackEditableCard is in edit mode (can drag to reorder)
/// </summary>
@ -34,8 +38,12 @@ public partial class StackExpanderViewModel : StackViewModelBase
[property: JsonIgnore]
private bool isEditEnabled;
[ObservableProperty]
private bool isEnabled;
/// <summary>
/// True to show the settings button, invokes <see cref="SettingsCommand"/> when clicked
/// </summary>
public virtual bool IsSettingsEnabled { get; set; }
public virtual IRelayCommand? SettingsCommand { get; set; }
/// <inheritdoc />
public StackExpanderViewModel(ServiceManager<ViewModelBase> vmFactory)

208
StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs

@ -1,7 +1,28 @@
using FluentAvalonia.UI.Controls;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Settings;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -9,12 +30,191 @@ namespace StabilityMatrix.Avalonia.ViewModels.Settings;
[View(typeof(InferenceSettingsPage))]
[Singleton, ManagedService]
public class InferenceSettingsViewModel : PageViewModelBase
public partial class InferenceSettingsViewModel : PageViewModelBase
{
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly ICompletionProvider completionProvider;
/// <inheritdoc />
public override string Title => "Inference";
/// <inheritdoc />
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
[ObservableProperty]
private bool isPromptCompletionEnabled = true;
[ObservableProperty]
private IReadOnlyList<string> availableTagCompletionCsvs = Array.Empty<string>();
[ObservableProperty]
private string? selectedTagCompletionCsv;
[ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true;
[ObservableProperty]
[CustomValidation(typeof(InferenceSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
private string? outputImageFileNameFormat;
[ObservableProperty]
private string? outputImageFileNameFormatSample;
public IEnumerable<FileNameFormatVar> OutputImageFileNameFormatVars =>
FileNameFormatProvider
.GetSample()
.Substitutions
.Select(kv => new FileNameFormatVar { Variable = $"{{{kv.Key}}}", Example = kv.Value.Invoke() });
[ObservableProperty]
private bool isImageViewerPixelGridEnabled = true;
public InferenceSettingsViewModel(
INotificationService notificationService,
IPrerequisiteHelper prerequisiteHelper,
IPyRunner pyRunner,
ServiceManager<ViewModelBase> dialogFactory,
ICompletionProvider completionProvider,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService,
INavigationService<SettingsViewModel> settingsNavigationService,
IAccountsService accountsService,
ISettingsManager settingsManager
)
{
this.settingsManager = settingsManager;
this.notificationService = notificationService;
this.completionProvider = completionProvider;
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedTagCompletionCsv,
settings => settings.TagCompletionCsv
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsPromptCompletionEnabled,
settings => settings.IsPromptCompletionEnabled,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsCompletionRemoveUnderscoresEnabled,
settings => settings.IsCompletionRemoveUnderscoresEnabled,
true
);
this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(formatProperty =>
{
var provider = FileNameFormatProvider.GetSample();
var template = formatProperty.Value ?? string.Empty;
if (!string.IsNullOrEmpty(template) && provider.Validate(template) == ValidationResult.Success)
{
var format = FileNameFormat.Parse(template, provider);
OutputImageFileNameFormatSample = format.GetFileName() + ".png";
}
else
{
// Use default format if empty
var defaultFormat = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, provider);
OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png";
}
});
settingsManager.RelayPropertyFor(
this,
vm => vm.OutputImageFileNameFormat,
settings => settings.InferenceOutputImageFileNameFormat,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsImageViewerPixelGridEnabled,
settings => settings.IsImageViewerPixelGridEnabled,
true
);
ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
}
/// <summary>
/// Validator for <see cref="OutputImageFileNameFormat"/>
/// </summary>
public static ValidationResult ValidateOutputImageFileNameFormat(string? format, ValidationContext context)
{
return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty);
}
/// <inheritdoc />
public override void OnLoaded()
{
base.OnLoaded();
UpdateAvailableTagCompletionCsvs();
}
#region Commands
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task ImportTagCsv()
{
var storage = App.StorageProvider;
var files = await storage.OpenFilePickerAsync(
new FilePickerOpenOptions
{
FileTypeFilter = new List<FilePickerFileType> { new("CSV") { Patterns = ["*.csv"] } }
}
);
if (files.Count == 0)
return;
var sourceFile = new FilePath(files[0].TryGetLocalPath()!);
var tagsDir = settingsManager.TagsDirectory;
tagsDir.Create();
// Copy to tags directory
var targetFile = tagsDir.JoinFile(sourceFile.Name);
await sourceFile.CopyToAsync(targetFile);
// Update index
UpdateAvailableTagCompletionCsvs();
// Trigger load
completionProvider.BackgroundLoadFromFile(targetFile, true);
notificationService.Show(
$"Imported {sourceFile.Name}",
$"The {sourceFile.Name} file has been imported.",
NotificationType.Success
);
}
#endregion
private void UpdateAvailableTagCompletionCsvs()
{
if (!settingsManager.IsLibraryDirSet)
return;
if (settingsManager.TagsDirectory is not { Exists: true } tagsDir)
return;
var csvFiles = tagsDir.Info.EnumerateFiles("*.csv");
AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray();
// Set selected to current if exists
var settingsCsv = settingsManager.Settings.TagCompletionCsv;
if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv))
{
SelectedTagCompletionCsv = settingsCsv;
}
}
}

274
StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs

@ -44,6 +44,7 @@ using StabilityMatrix.Avalonia.Views.Settings;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Python;
@ -73,8 +74,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
public SharedState SharedState { get; }
public override string Title => "Settings";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
// ReSharper disable once MemberCanBeMadeStatic.Global
public string AppVersion =>
@ -102,54 +102,35 @@ public partial class MainSettingsViewModel : PageViewModelBase
[ObservableProperty]
private bool removeSymlinksOnShutdown;
// Inference UI section
// Integrations section
[ObservableProperty]
private bool isPromptCompletionEnabled = true;
private bool isDiscordRichPresenceEnabled;
// Debug section
[ObservableProperty]
private IReadOnlyList<string> availableTagCompletionCsvs = Array.Empty<string>();
private string? debugPaths;
[ObservableProperty]
private string? selectedTagCompletionCsv;
private string? debugCompatInfo;
[ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true;
private string? debugGpuInfo;
[ObservableProperty]
[CustomValidation(typeof(MainSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
private string? outputImageFileNameFormat;
#region System Info
[ObservableProperty]
private string? outputImageFileNameFormatSample;
public IEnumerable<FileNameFormatVar> OutputImageFileNameFormatVars =>
FileNameFormatProvider
.GetSample()
.Substitutions.Select(
kv =>
new FileNameFormatVar
{
Variable = $"{{{kv.Key}}}",
Example = kv.Value.Invoke()
}
);
private static Lazy<IReadOnlyList<GpuInfo>> GpuInfosLazy { get; } =
new(() => HardwareHelper.IterGpuInfo().ToImmutableArray());
[ObservableProperty]
private bool isImageViewerPixelGridEnabled = true;
public static IReadOnlyList<GpuInfo> GpuInfos => GpuInfosLazy.Value;
// Integrations section
[ObservableProperty]
private bool isDiscordRichPresenceEnabled;
private MemoryInfo memoryInfo;
// Debug section
[ObservableProperty]
private string? debugPaths;
private readonly DispatcherTimer hardwareInfoUpdateTimer = new() { Interval = TimeSpan.FromSeconds(2.627) };
[ObservableProperty]
private string? debugCompatInfo;
public Task<CpuInfo> CpuInfoAsync => HardwareHelper.GetCpuInfoAsync();
[ObservableProperty]
private string? debugGpuInfo;
#endregion
// Info section
private const int VersionTapCountThreshold = 7;
@ -162,8 +143,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
public string VersionFlyoutText =>
$"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options.";
public string DataDirectory =>
settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set";
public string DataDirectory => settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set";
public MainSettingsViewModel(
INotificationService notificationService,
@ -211,77 +191,28 @@ public partial class MainSettingsViewModel : PageViewModelBase
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale
);
settingsManager.RelayPropertyFor(this, vm => vm.SelectedAnimationScale, settings => settings.AnimationScale);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedTagCompletionCsv,
settings => settings.TagCompletionCsv
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsPromptCompletionEnabled,
settings => settings.IsPromptCompletionEnabled,
true
);
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsCompletionRemoveUnderscoresEnabled,
settings => settings.IsCompletionRemoveUnderscoresEnabled,
true
);
this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(formatProperty =>
{
var provider = FileNameFormatProvider.GetSample();
var template = formatProperty.Value ?? string.Empty;
hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick;
}
if (
!string.IsNullOrEmpty(template)
&& provider.Validate(template) == ValidationResult.Success
)
{
var format = FileNameFormat.Parse(template, provider);
OutputImageFileNameFormatSample = format.GetFileName() + ".png";
}
else
{
// Use default format if empty
var defaultFormat = FileNameFormat.Parse(
FileNameFormat.DefaultTemplate,
provider
);
OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png";
}
});
/// <inheritdoc />
public override void OnLoaded()
{
base.OnLoaded();
settingsManager.RelayPropertyFor(
this,
vm => vm.OutputImageFileNameFormat,
settings => settings.InferenceOutputImageFileNameFormat,
true
);
hardwareInfoUpdateTimer.Start();
OnHardwareInfoUpdateTimerTick(null, null!);
}
settingsManager.RelayPropertyFor(
this,
vm => vm.IsImageViewerPixelGridEnabled,
settings => settings.IsImageViewerPixelGridEnabled,
true
);
/// <inheritdoc />
public override void OnUnloaded()
{
base.OnUnloaded();
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(
notificationService,
LogLevel.Warn
);
ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
hardwareInfoUpdateTimer.Stop();
}
/// <inheritdoc />
@ -291,18 +222,13 @@ public partial class MainSettingsViewModel : PageViewModelBase
await notificationService.TryAsync(completionProvider.Setup());
UpdateAvailableTagCompletionCsvs();
// Start accounts update
accountsService.RefreshAsync().SafeFireAndForget();
}
public static ValidationResult ValidateOutputImageFileNameFormat(
string? format,
ValidationContext context
)
private void OnHardwareInfoUpdateTimerTick(object? sender, EventArgs e)
{
return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty);
MemoryInfo = HardwareHelper.GetMemoryInfo();
}
partial void OnSelectedThemeChanged(string? value)
@ -341,22 +267,20 @@ public partial class MainSettingsViewModel : PageViewModelBase
CloseButtonText = Resources.Action_RelaunchLater
};
Dispatcher.UIThread.InvokeAsync(async () =>
{
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
Dispatcher
.UIThread
.InvokeAsync(async () =>
{
Process.Start(Compat.AppCurrentPath);
App.Shutdown();
}
});
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
Process.Start(Compat.AppCurrentPath);
App.Shutdown();
}
});
}
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);
}
}
@ -379,14 +303,16 @@ public partial class MainSettingsViewModel : PageViewModelBase
[RelayCommand]
private void NavigateToSubPage(Type viewModelType)
{
Dispatcher.UIThread.Post(
() =>
settingsNavigationService.NavigateTo(
viewModelType,
BetterSlideNavigationTransition.PageSlideFromRight
),
DispatcherPriority.Send
);
Dispatcher
.UIThread
.Post(
() =>
settingsNavigationService.NavigateTo(
viewModelType,
BetterSlideNavigationTransition.PageSlideFromRight
),
DispatcherPriority.Send
);
}
#region Package Environment
@ -397,8 +323,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
var viewModel = dialogFactory.Get<EnvVarsViewModel>();
// Load current settings
var current =
settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
var current = settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>(
current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value))
);
@ -414,7 +339,8 @@ public partial class MainSettingsViewModel : PageViewModelBase
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
// Save settings
var newEnvVars = viewModel.EnvVars
var newEnvVars = viewModel
.EnvVars
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key))
.GroupBy(kvp => kvp.Key, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First().Value, StringComparer.Ordinal);
@ -453,68 +379,6 @@ public partial class MainSettingsViewModel : PageViewModelBase
#endregion
#region Inference UI
private void UpdateAvailableTagCompletionCsvs()
{
if (!settingsManager.IsLibraryDirSet)
return;
var tagsDir = settingsManager.TagsDirectory;
if (!tagsDir.Exists)
return;
var csvFiles = tagsDir.Info.EnumerateFiles("*.csv");
AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray();
// Set selected to current if exists
var settingsCsv = settingsManager.Settings.TagCompletionCsv;
if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv))
{
SelectedTagCompletionCsv = settingsCsv;
}
}
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task ImportTagCsv()
{
var storage = App.StorageProvider;
var files = await storage.OpenFilePickerAsync(
new FilePickerOpenOptions
{
FileTypeFilter = new List<FilePickerFileType>
{
new("CSV") { Patterns = new[] { "*.csv" }, }
}
}
);
if (files.Count == 0)
return;
var sourceFile = new FilePath(files[0].TryGetLocalPath()!);
var tagsDir = settingsManager.TagsDirectory;
tagsDir.Create();
// Copy to tags directory
var targetFile = tagsDir.JoinFile(sourceFile.Name);
await sourceFile.CopyToAsync(targetFile);
// Update index
UpdateAvailableTagCompletionCsvs();
// Trigger load
completionProvider.BackgroundLoadFromFile(targetFile, true);
notificationService.Show(
$"Imported {sourceFile.Name}",
$"The {sourceFile.Name} file has been imported.",
NotificationType.Success
);
}
#endregion
#region System
/// <summary>
@ -531,10 +395,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
"Programs"
);
var shortcutDir = new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs");
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
@ -566,8 +427,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
// 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 = Resources.Action_Yes,
CloseButtonText = Resources.Action_Cancel,
@ -764,16 +624,12 @@ public partial class MainSettingsViewModel : PageViewModelBase
private async Task DebugMakeImageGrid()
{
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(
new FilePickerOpenOptions() { AllowMultiple = true }
);
var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions() { AllowMultiple = true });
if (files.Count == 0)
return;
var images = await files.SelectAsync(
async f => SKImage.FromEncodedData(await f.OpenReadAsync())
);
var images = await files.SelectAsync(async f => SKImage.FromEncodedData(await f.OpenReadAsync()));
var grid = ImageProcessor.CreateImageGrid(images.ToImmutableArray());
@ -923,11 +779,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
}
catch (Exception e)
{
notificationService.Show(
"Failed to read licenses information",
$"{e}",
NotificationType.Error
);
notificationService.Show("Failed to read licenses information", $"{e}", NotificationType.Error);
}
}

5
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -18,13 +18,12 @@ namespace StabilityMatrix.Avalonia.ViewModels;
public partial class SettingsViewModel : PageViewModelBase
{
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 };
public IReadOnlyList<PageViewModelBase> SubPages { get; }
[ObservableProperty]
private ObservableCollection<PageViewModelBase> currentPagePath = new();
private ObservableCollection<PageViewModelBase> currentPagePath = [];
[ObservableProperty]
private PageViewModelBase? currentPage;

549
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -1,531 +1,18 @@
<UserControl
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="viewModels:CheckpointBrowserViewModel"
mc:Ignorable="d">
<UserControl.Styles>
<Style Selector="Border#HoverBorder">
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237"/>
</Transitions>
</Setter>
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.237">
<TransformOperationsTransition.Easing>
<QuadraticEaseInOut/>
</TransformOperationsTransition.Easing>
</TransformOperationsTransition>
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/>
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#CC000000" />
</Style>
</Style>
<Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/>
<Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="RenderTransform" Value="scale(1, 1)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#99000000" />
</Style>
</Style>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}"
Command="{Binding OpenModelCommand}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<Button
Name="ModelCard"
Classes="transparent-full"
Padding="0"
BorderThickness="0"
VerticalContentAlignment="Top"
CornerRadius="8"
Command="{Binding ShowVersionDialogCommand}"
CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}">
<Grid RowDefinitions="*, Auto">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding CardImage}"
Stretch="UniformToFill"
StretchDirection="Both"/>
<StackPanel
Grid.Row="0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="transparent-info"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding !IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarAdd" />
</Grid>
</Button>
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="success"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarFilled" />
</Grid>
</Button>
</StackPanel>
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9"/>
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2"/>
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<controls:BetterAdvancedImage
Width="22"
Height="22"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding CivitModel.Creator.Image}"/>
<TextBlock
VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding CivitModel.Creator.Username}"/>
</StackPanel>
</Button>
</Border>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<controls:Card
Height="24"
Margin="8,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.Type}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.BaseModelType}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="success"
IsVisible="{Binding ShowUpdateCard}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding UpdateCardText}" />
</controls:Card>
</StackPanel>
<Border
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="#DD000000"
CornerRadius="8"
IsVisible="{Binding IsImporting}"
ZIndex="1" />
<StackPanel
Grid.Row="0"
Grid.RowSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding IsImporting}"
Orientation="Vertical"
ZIndex="2">
<controls:ProgressRing
Width="120"
Height="120"
HorizontalAlignment="Center"
VerticalAlignment="Center"
EndAngle="450"
IsIndeterminate="False"
StartAngle="90"
Value="{Binding Value}" />
<TextBlock
Margin="0,8,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Text, TargetNullValue=Importing...}" />
</StackPanel>
<Border
Name="ModelCardBottom"
Grid.Row="1">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto">
<!--
TextTrimming causing issues with unicode chars until
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released
-->
<TextBlock
Grid.ColumnSpan="2"
MaxWidth="250"
Margin="8,0,8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource TextControlForeground}"
LetterSpacing="0.33"
Text="{Binding CivitModel.Name}"
TextWrapping="NoWrap"
ToolTip.Tip="{Binding CivitModel.Name}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="8,-4,0,0"
VerticalAlignment="Center"
FontSize="11"
Foreground="{DynamicResource TextControlForeground}"
Text="{Binding CivitModel.LatestModelVersionName, FallbackValue=''}" />
<StackPanel
Grid.Row="2"
Grid.Column="0"
Orientation="Horizontal">
<controls:StarsRating
Margin="8,8,0,8"
Background="#66000000"
FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}"
TextAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<avalonia:Icon Value="fa-solid fa-heart" />
<TextBlock
Margin="4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
Margin="0,0,4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.DownloadCount, Converter={StaticResource KiloFormatterConverter}}" />
</StackPanel>
<Button
Grid.Row="0"
Grid.Column="1"
Width="32"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{Binding OpenModelCommand}" Header="{x:Static lang:Resources.Action_OpenOnCivitAi}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Border>
</Grid>
</Button>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid Margin="0,8,0,0" RowDefinitions="Auto,*,Auto">
<StackPanel Margin="8" Orientation="Vertical">
<Grid ColumnDefinitions="*,Auto">
<TextBox
Margin="8,0,0,0"
HorizontalAlignment="Stretch"
KeyDown="InputElement_OnKeyDown"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}">
<TextBox.InnerRightContent>
<Button
Classes="transparent-full"
Command="{Binding ClearSearchQuery}"
IsVisible="{Binding SearchQuery.Length}">
<ui:SymbolIcon Symbol="Cancel" />
</Button>
</TextBox.InnerRightContent>
</TextBox>
<Button
Grid.Column="1"
Width="80"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchModelsCommand}"
IsDefault="True">
<Grid>
<controls:ProgressRing
MinWidth="16"
MinHeight="16"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding SearchModelsCommand.IsRunning}" />
<TextBlock
VerticalAlignment="Center"
IsVisible="{Binding !SearchModelsCommand.IsRunning}"
Text="{x:Static lang:Resources.Action_Search}" />
</Grid>
</Button>
</Grid>
<DockPanel>
<StackPanel Margin="8,8,4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllSortModes}"
SelectedItem="{Binding SortMode}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllCivitPeriods}"
SelectedItem="{Binding SelectedPeriod}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_ModelType}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllModelTypes}"
SelectedItem="{Binding SelectedModelType}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_BaseModel}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding BaseModelOptions}"
SelectedItem="{Binding SelectedBaseModelType}" />
</StackPanel>
<CheckBox
Margin="8,8,8,0"
HorizontalAlignment="Right"
Content="{x:Static lang:Resources.Label_ShowNsfwContent}"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" />
</DockPanel>
</StackPanel>
<ScrollViewer
Grid.Row="1"
Margin="8,0,8,0"
ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}"
HorizontalAlignment="Center"
ItemsSource="{Binding ModelCards}">
<ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<TextBlock
Grid.Row="2"
Margin="16,8"
VerticalAlignment="Bottom"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" />
<StackPanel
Grid.Row="2"
Margin="8"
HorizontalAlignment="Center"
IsVisible="{Binding HasSearched}"
Orientation="Vertical">
<TextBlock Margin="0,0,4,4" TextAlignment="Center">
<Run Text="{x:Static lang:Resources.Label_Page}" />
<Run Text="{Binding CurrentPageNumber, FallbackValue=1}" />
<Run Text="/" />
<Run Text="{Binding TotalPages, FallbackValue=5}" />
</TextBlock>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,8,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<Button
Margin="0,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
</StackPanel>
<TextBlock
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
IsVisible="{Binding NoResultsFound}"
Text="{Binding NoResultsText, FallbackValue=No results found}" />
<controls:ProgressRing
Grid.Row="1"
Width="128"
Height="128"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsIndeterminate="True"
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" />
</Grid>
</UserControl>
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="viewModels:CheckpointBrowserViewModel"
mc:Ignorable="d"
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage">
<TabControl ItemsSource="{Binding Pages}"/>
</controls:UserControlBase>

29
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs

@ -1,9 +1,4 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views;
@ -15,26 +10,4 @@ public partial class CheckpointBrowserPage : UserControlBase
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (sender is not ScrollViewer scrollViewer)
return;
var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum;
Debug.WriteLine($"IsAtEnd: {isAtEnd}");
}
private void InputElement_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape && DataContext is CheckpointBrowserViewModel viewModel)
{
viewModel.ClearSearchQuery();
}
}
}

91
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -56,14 +56,13 @@
</Interaction.Behaviors>
<controls:Card MaxWidth="330" Width="330"
Padding="0"
CornerRadius="12">
<!-- Right click menu for a checkpoint file -->
<controls:Card.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem Command="{Binding RenameCommand}"
Text="{x:Static lang:Resources.Action_Rename}" IconSource="Rename" />
<ui:MenuFlyoutItem Command="{Binding DeleteCommand}"
Text="{x:Static lang:Resources.Action_Delete}" IconSource="Delete" />
<ui:MenuFlyoutItem Command="{Binding OpenOnCivitAiCommand}"
Text="{x:Static lang:Resources.Action_OpenOnCivitAi}" IconSource="Link"
IsVisible="{Binding IsConnectedModel}" />
@ -71,11 +70,31 @@
IsVisible="{Binding CanShowTriggerWords}"
Text="{x:Static lang:Resources.Action_CopyTriggerWords}"
IconSource="Copy" />
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Label_FindConnectedMetadata}"
IconSource="Find"
Command="{Binding FindConnectedMetadataCommand}"
IsVisible="{Binding !IsConnectedModel}">
<ui:MenuFlyoutItem.CommandParameter>
<system:Boolean>False</system:Boolean>
</ui:MenuFlyoutItem.CommandParameter>
</ui:MenuFlyoutItem>
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Action_UpdateExistingMetadata}"
IconSource="Sync"
Command="{Binding FindConnectedMetadataCommand}"
IsVisible="{Binding IsConnectedModel}">
<ui:MenuFlyoutItem.CommandParameter>
<system:Boolean>True</system:Boolean>
</ui:MenuFlyoutItem.CommandParameter>
</ui:MenuFlyoutItem>
<ui:MenuFlyoutItem Command="{Binding DeleteCommand}"
Text="{x:Static lang:Resources.Action_Delete}"
IconSource="Delete" />
</ui:FAMenuFlyout>
</controls:Card.ContextFlyout>
<Grid>
<Grid RowDefinitions="Auto, Auto">
<!-- Main contents, hidden when IsLoading is true -->
<StackPanel MinHeight="70">
<StackPanel Grid.Row="0" Grid.RowSpan="2"
MinHeight="70" Margin="14, 16, 14, 16">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto,*,Auto,Auto"
IsVisible="{Binding !IsLoading}">
<StackPanel
@ -229,7 +248,20 @@
</TextBlock>
</Grid>
</StackPanel>
<Border Grid.Row="0"
Grid.RowSpan="2"
Background="#AA000000"
IsVisible="{Binding IsLoading}"
CornerRadius="12"/>
<TextBlock Grid.Row="1"
x:CompileBindings="False"
Text="{Binding Progress.Title}"
TextAlignment="Center"
IsVisible="{Binding IsLoading}"
VerticalAlignment="Center"/>
<!-- Progress ring -->
<controls:ProgressRing
Grid.Row="0"
@ -407,7 +439,7 @@
</controls:UserControlBase.Resources>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*" Margin="4, 0"
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,*" Margin="4, 0"
x:Name="ParentGrid">
<!-- Top settings bar -->
<StackPanel Orientation="Horizontal">
@ -463,6 +495,36 @@
HorizontalContentAlignment="Right"
DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands>
<ui:CommandBarElementContainer>
<SplitButton
Classes="transparent-full"
HorizontalAlignment="Left"
FontFamily="{DynamicResource ContentControlThemeFontFamily}"
FontWeight="Normal"
FontSize="12"
Command="{Binding FindConnectedMetadataCommand}"
Padding="8">
<SplitButton.Content>
<Grid ColumnDefinitions="Auto, Auto">
<ui:SymbolIcon Grid.Column="0"
Symbol="CloudDownload"
FontSize="18"/>
<TextBlock Grid.Column="1"
Text="{x:Static lang:Resources.Label_FindConnectedMetadata}"
Margin="8,0,0,0"
VerticalAlignment="Center"/>
</Grid>
</SplitButton.Content>
<SplitButton.Flyout>
<ui:FAMenuFlyout
Placement="Bottom">
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Action_UpdateExistingMetadata}"
Command="{Binding UpdateExistingMetadataCommand}"/>
</ui:FAMenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</ui:CommandBarElementContainer>
<ui:CommandBarButton
IconSource="OpenFolder"
VerticalAlignment="Center"
@ -494,6 +556,19 @@
Title="{x:Static lang:Resources.TeachingTip_MoreCheckpointCategories}"
PreferredPlacement="Bottom"
IsOpen="{Binding IsCategoryTipOpen}" />
<!-- Metadata scan progress -->
<StackPanel Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
IsVisible="{Binding !!Progress.Total}"
Orientation="Vertical">
<TextBlock Text="{Binding Progress.Title}"
TextAlignment="Center"
Margin="0,0,0,4"/>
<ProgressBar Value="{Binding Progress.Percentage}"
Margin="16,4,16,16"/>
</StackPanel>
<StackPanel
IsVisible="False"
@ -519,7 +594,7 @@
<ScrollViewer
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="1"
Grid.Row="2"
x:Name="MainScrollViewer"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
@ -546,7 +621,7 @@
<!-- Overlay for draggable file panels -->
<Panel Name="OverlayPanel"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" />
Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" />
</Grid>
</controls:UserControlBase>

35
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs

@ -44,8 +44,7 @@ public partial class CheckpointsPage : UserControlBase
if (DataContext is CheckpointsPageViewModel vm)
{
subscription = vm.WhenPropertyChanged(m => m.ShowConnectedModelImages)
.Subscribe(_ => InvalidateRepeater());
subscription = vm.WhenPropertyChanged(m => m.ShowConnectedModelImages).Subscribe(_ => InvalidateRepeater());
}
}
@ -70,7 +69,10 @@ public partial class CheckpointsPage : UserControlBase
case CheckpointFolder folder:
{
if (e.Data.Get("Context") is not CheckpointFile file)
return;
{
await folder.OnDrop(e);
break;
}
var filePath = new FilePath(file.FilePath);
if (filePath.Directory?.FullPath != folder.DirectoryPath)
@ -82,7 +84,10 @@ public partial class CheckpointsPage : UserControlBase
case CheckpointFile file:
{
if (e.Data.Get("Context") is not CheckpointFile dragFile)
return;
{
await file.ParentFolder.OnDrop(e);
break;
}
var parentFolder = file.ParentFolder;
var dragFilePath = new FilePath(dragFile.FilePath);
@ -98,9 +103,14 @@ public partial class CheckpointsPage : UserControlBase
private static void OnDragExit(object? sender, DragEventArgs e)
{
var sourceDataContext = (e.Source as Control)?.DataContext;
if (sourceDataContext is CheckpointFolder folder)
switch (sourceDataContext)
{
folder.IsCurrentDragTarget = false;
case CheckpointFolder folder:
folder.IsCurrentDragTarget = false;
break;
case CheckpointFile file:
file.ParentFolder.IsCurrentDragTarget = false;
break;
}
}
@ -123,7 +133,10 @@ public partial class CheckpointsPage : UserControlBase
{
folder.IsExpanded = true;
if (e.Data.Get("Context") is not CheckpointFile file)
return;
{
folder.IsCurrentDragTarget = true;
break;
}
var filePath = new FilePath(file.FilePath);
folder.IsCurrentDragTarget = filePath.Directory?.FullPath != folder.DirectoryPath;
@ -132,12 +145,14 @@ public partial class CheckpointsPage : UserControlBase
case CheckpointFile file:
{
if (e.Data.Get("Context") is not CheckpointFile dragFile)
return;
{
file.ParentFolder.IsCurrentDragTarget = true;
break;
}
var parentFolder = file.ParentFolder;
var dragFilePath = new FilePath(dragFile.FilePath);
parentFolder.IsCurrentDragTarget =
dragFilePath.Directory?.FullPath != parentFolder.DirectoryPath;
parentFolder.IsCurrentDragTarget = dragFilePath.Directory?.FullPath != parentFolder.DirectoryPath;
break;
}
}

531
StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml

@ -0,0 +1,531 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.CivitAiBrowserPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="checkpointBrowser:CivitAiBrowserViewModel"
mc:Ignorable="d">
<UserControl.Styles>
<Style Selector="Border#HoverBorder">
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237"/>
</Transitions>
</Setter>
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.237">
<TransformOperationsTransition.Easing>
<QuadraticEaseInOut/>
</TransformOperationsTransition.Easing>
</TransformOperationsTransition>
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/>
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#CC000000" />
</Style>
</Style>
<Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/>
<Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="RenderTransform" Value="scale(1, 1)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#99000000" />
</Style>
</Style>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}"
Command="{Binding OpenModelCommand}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<Button
Name="ModelCard"
Classes="transparent-full"
Padding="0"
BorderThickness="0"
VerticalContentAlignment="Top"
CornerRadius="8"
Command="{Binding ShowVersionDialogCommand}"
CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}">
<Grid RowDefinitions="*, Auto">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding CardImage}"
Stretch="UniformToFill"
StretchDirection="Both"/>
<StackPanel
Grid.Row="0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="transparent-info"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding !IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarAdd" />
</Grid>
</Button>
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="success"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarFilled" />
</Grid>
</Button>
</StackPanel>
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9"/>
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2"/>
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<controls:BetterAdvancedImage
Width="22"
Height="22"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding CivitModel.Creator.Image}"/>
<TextBlock
VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding CivitModel.Creator.Username}"/>
</StackPanel>
</Button>
</Border>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<controls:Card
Height="24"
Margin="8,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.Type}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.BaseModelType}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="success"
IsVisible="{Binding ShowUpdateCard}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding UpdateCardText}" />
</controls:Card>
</StackPanel>
<Border
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="#DD000000"
CornerRadius="8"
IsVisible="{Binding IsImporting}"
ZIndex="1" />
<StackPanel
Grid.Row="0"
Grid.RowSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding IsImporting}"
Orientation="Vertical"
ZIndex="2">
<controls:ProgressRing
Width="120"
Height="120"
HorizontalAlignment="Center"
VerticalAlignment="Center"
EndAngle="450"
IsIndeterminate="False"
StartAngle="90"
Value="{Binding Value}" />
<TextBlock
Margin="0,8,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Text, TargetNullValue=Importing...}" />
</StackPanel>
<Border
Name="ModelCardBottom"
Grid.Row="1">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto">
<!--
TextTrimming causing issues with unicode chars until
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released
-->
<TextBlock
Grid.ColumnSpan="2"
MaxWidth="250"
Margin="8,0,8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource TextControlForeground}"
LetterSpacing="0.33"
Text="{Binding CivitModel.Name}"
TextWrapping="NoWrap"
ToolTip.Tip="{Binding CivitModel.Name}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="8,-4,0,0"
VerticalAlignment="Center"
FontSize="11"
Foreground="{DynamicResource TextControlForeground}"
Text="{Binding CivitModel.LatestModelVersionName, FallbackValue=''}" />
<StackPanel
Grid.Row="2"
Grid.Column="0"
Orientation="Horizontal">
<controls:StarsRating
Margin="8,8,0,8"
Background="#66000000"
FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}"
TextAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<avalonia:Icon Value="fa-solid fa-heart" />
<TextBlock
Margin="4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
Margin="0,0,4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.DownloadCount, Converter={StaticResource KiloFormatterConverter}}" />
</StackPanel>
<Button
Grid.Row="0"
Grid.Column="1"
Width="32"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{Binding OpenModelCommand}" Header="{x:Static lang:Resources.Action_OpenOnCivitAi}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Border>
</Grid>
</Button>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid RowDefinitions="Auto,*,Auto">
<StackPanel Margin="8" Orientation="Vertical">
<Grid ColumnDefinitions="*,Auto">
<TextBox
HorizontalAlignment="Stretch"
KeyDown="InputElement_OnKeyDown"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}">
<TextBox.InnerRightContent>
<Button
Classes="transparent-full"
Command="{Binding ClearSearchQuery}"
IsVisible="{Binding SearchQuery.Length}">
<ui:SymbolIcon Symbol="Cancel" />
</Button>
</TextBox.InnerRightContent>
</TextBox>
<Button
Grid.Column="1"
Width="80"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchModelsCommand}"
IsDefault="True">
<Grid>
<controls:ProgressRing
MinWidth="16"
MinHeight="16"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding SearchModelsCommand.IsRunning}" />
<TextBlock
VerticalAlignment="Center"
IsVisible="{Binding !SearchModelsCommand.IsRunning}"
Text="{x:Static lang:Resources.Action_Search}" />
</Grid>
</Button>
</Grid>
<DockPanel>
<StackPanel Margin="0,8,4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllSortModes}"
SelectedItem="{Binding SortMode}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllCivitPeriods}"
SelectedItem="{Binding SelectedPeriod}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_ModelType}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllModelTypes}"
SelectedItem="{Binding SelectedModelType}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_BaseModel}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding BaseModelOptions}"
SelectedItem="{Binding SelectedBaseModelType}" />
</StackPanel>
<CheckBox
Margin="8,8,0,0"
HorizontalAlignment="Right"
Content="{x:Static lang:Resources.Label_ShowNsfwContent}"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" />
</DockPanel>
</StackPanel>
<ScrollViewer
Grid.Row="1"
ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}"
HorizontalAlignment="Center"
ItemsSource="{Binding ModelCards}">
<ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<TextBlock
Grid.Row="2"
Margin="8,8"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" />
<StackPanel Grid.Row="2"
HorizontalAlignment="Center"
IsVisible="{Binding HasSearched}"
Margin="0,8,0,8"
Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,16,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<TextBlock Margin="8,0,4,0" TextAlignment="Center"
Text="{x:Static lang:Resources.Label_Page}"
VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding CurrentPageNumber, FallbackValue=1}"
VerticalAlignment="Center"
SpinButtonPlacementMode="Hidden"
TextAlignment="Center"/>
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center">
<Run Text="/"/>
<Run Text="{Binding TotalPages, FallbackValue=5}"/>
</TextBlock>
<Button
Margin="16,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
<TextBlock
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
IsVisible="{Binding NoResultsFound}"
Text="{Binding NoResultsText, FallbackValue=No results found}" />
<controls:ProgressRing
Grid.Row="1"
Width="128"
Height="128"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsIndeterminate="True"
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" />
</Grid>
</controls:UserControlBase>

41
StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs

@ -0,0 +1,41 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Core.Attributes;
using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel;
namespace StabilityMatrix.Avalonia.Views;
[Singleton]
public partial class CivitAiBrowserPage : UserControlBase
{
public CivitAiBrowserPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (sender is not ScrollViewer scrollViewer)
return;
var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum;
Debug.WriteLine($"IsAtEnd: {isAtEnd}");
}
private void InputElement_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape && DataContext is CivitAiBrowserViewModel viewModel)
{
viewModel.ClearSearchQuery();
}
}
}

2
StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml

@ -30,7 +30,7 @@
IsOpen="{Binding IsInstallMode}"
IsVisible="{Binding IsInstallMode}"
Margin="0,4"
Title="Would you like to be directed to install ComfyUI now?"/>
Title="Would you like to install ComfyUI now?"/>
<ui:InfoBar
Severity="Informational"

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

@ -34,18 +34,22 @@ public partial class PackageModificationDialog : UserControlBase
textMate.SetGrammar(scope);
textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus));
editor.Options.ShowBoxForControlCharacters = false;
}
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);
});
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);
});
};
}
}

33
StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml

@ -0,0 +1,33 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PropertyGridDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=System.ObjectModel"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:vmDialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
d:DataContext="{x:Static mocks:DesignData.PropertyGridViewModel}"
d:DesignHeight="450"
d:DesignWidth="600"
x:DataType="vmDialogs:PropertyGridViewModel"
mc:Ignorable="d">
<Grid>
<ItemsControl x:Name="MainItemsControl" ItemsSource="{Binding SelectedObjectItemsSource, Mode=OneTime}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="componentModel:INotifyPropertyChanged">
<controls:BetterPropertyGrid
AllowFilter="False"
AllowQuickFilter="False"
SelectedObject="{Binding Mode=OneTime}"
IncludedCategories="{Binding $parent[ItemsControl].((vmDialogs:PropertyGridViewModel)DataContext).IncludeCategories, Mode=OneTime, FallbackValue=''}"
ExcludedCategories="{Binding $parent[ItemsControl].((vmDialogs:PropertyGridViewModel)DataContext).ExcludeCategories, Mode=OneTime, FallbackValue=''}"
ShowStyle="{Binding $parent[ItemsControl].((vmDialogs:PropertyGridViewModel)DataContext).ShowStyle, Mode=OneTime, FallbackValue=Alphabetic}"
ShowTitle="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</controls:UserControlBase>

13
StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs

@ -0,0 +1,13 @@
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
[Transient]
public partial class PropertyGridDialog : UserControlBase
{
public PropertyGridDialog()
{
InitializeComponent();
}
}

188
StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml

@ -0,0 +1,188 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.HuggingFacePage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:huggingFacePage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.HuggingFacePage"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
d:DataContext="{x:Static mocks:DesignData.HuggingFacePageViewModel}"
d:DesignHeight="650"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="checkpointBrowser:HuggingFacePageViewModel"
Focusable="True"
mc:Ignorable="d">
<ScrollViewer>
<Grid RowDefinitions="Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto, *"
Margin="8"
HorizontalAlignment="Stretch">
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
IsVisible="{Binding !!NumSelected}"
Margin="8,0,8,0">
<Run Text="{Binding NumSelected}" />
<Run Text="models selected" />
</TextBlock>
<ui:CommandBar
Grid.Row="0"
Grid.Column="0"
Margin="0,-1,0,0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
VerticalContentAlignment="Center"
DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands>
<ui:CommandBarButton
IsEnabled="{Binding !!NumSelected}"
IconSource="Download"
Foreground="{DynamicResource AccentButtonBackground}"
Command="{Binding ImportSelectedCommand}"
Label="{x:Static lang:Resources.Action_Import}" />
<ui:CommandBarSeparator />
<ui:CommandBarButton
Command="{Binding SelectAll}"
IconSource="SelectAll"
Label="{x:Static lang:Resources.Action_SelectAll}" />
<ui:CommandBarButton
IconSource="ClearSelection"
Command="{Binding ClearSelection}"
IsEnabled="{Binding !!NumSelected}"
Label="{x:Static lang:Resources.Action_ClearSelection}" />
<ui:CommandBarSeparator IsVisible="{Binding !!NumSelected}" />
</ui:CommandBar.PrimaryCommands>
</ui:CommandBar>
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Orientation="Vertical"
Margin="0,8"
VerticalAlignment="Bottom">
<TextBlock Text="{Binding DownloadPercentText}"
TextAlignment="Center"
IsVisible="{Binding !!TotalProgress.Total}"
Margin="0,4"
HorizontalAlignment="Stretch" />
<ProgressBar Value="{Binding TotalProgress.Current}"
Maximum="{Binding TotalProgress.Total}"
IsVisible="{Binding !!TotalProgress.Total}"
IsIndeterminate="False" />
</StackPanel>
<ItemsRepeater Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
ItemsSource="{Binding Categories}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Vertical" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type huggingFacePage:CategoryViewModel}">
<Expander IsExpanded="True" Margin="0,0,0,8">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}"
Content="{Binding Title}" />
</StackPanel>
</Expander.Header>
<ItemsRepeater ItemsSource="{Binding Items}"
Margin="8,0">
<ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="8"
MinRowSpacing="8"
MinItemWidth="220"
ItemsStretch="Uniform" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type huggingFacePage:HuggingfaceItemViewModel}">
<Button
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding ToggleSelectedCommand}"
CornerRadius="8">
<Button.ContextFlyout>
<MenuFlyout>
<MenuItem Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding RepoUrl}"
Header="{x:Static lang:Resources.Action_OpenOnHuggingFace}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{x:Static lang:Resources.Label_License}"
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding LicenseUrl}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Document" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.ContextFlyout>
<Grid ColumnDefinitions="Auto, *">
<CheckBox
VerticalAlignment="Center"
IsChecked="{Binding IsSelected}"
Content="{Binding Item.ModelName}" />
<Button
Grid.Column="1"
Width="24"
Margin="0,4,4,0"
Padding="4"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding RepoUrl}"
Header="{x:Static lang:Resources.Action_OpenOnHuggingFace}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{x:Static lang:Resources.Label_License}"
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding LicenseUrl}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="List" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Expander>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<!-- <Expander Grid.Column="0" -->
<!-- Grid.ColumnSpan="2" -->
<!-- Grid.Row="3" -->
<!-- Header="Manual Model Download"> -->
<!-- <Grid RowDefinitions="Auto, Auto" ColumnDefinitions="*, Auto"> -->
<!-- <TextBox Grid.Row="1" Grid.Column="0" -->
<!-- Watermark="Enter HuggingFace repo URL" -->
<!-- Margin="0,8" /> -->
<!-- <Button Grid.Row="1" Grid.Column="1" -->
<!-- Margin="8,8,0,8" -->
<!-- Classes="accent" -->
<!-- Content="Import" /> -->
<!-- </Grid> -->
<!-- </Expander> -->
</Grid>
</ScrollViewer>
</controls:UserControlBase>

13
StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml.cs

@ -0,0 +1,13 @@
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views;
[Singleton]
public partial class HuggingFacePage : UserControlBase
{
public HuggingFacePage()
{
InitializeComponent();
}
}

148
StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml

@ -4,66 +4,144 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings"
d:DataContext="{x:Static mocks:DesignData.InferenceSettingsViewModel}"
d:DesignHeight="450"
d:DesignWidth="800"
d:DesignHeight="650"
d:DesignWidth="900"
x:DataType="vmSettings:InferenceSettingsViewModel"
mc:Ignorable="d">
<controls:UserControlBase.Styles>
<Style Selector="sg|SpacedGrid &gt; ui|SettingsExpander">
<Setter Property="Margin" Value="8,0" />
</Style>
</controls:UserControlBase.Styles>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
<Grid RowDefinitions="Auto,*,*,*">
<!-- Prompt -->
<sg:SpacedGrid RowDefinitions="Auto,*" RowSpacing="4">
<TextBlock
Margin="0,0,0,8"
Margin="0,0,0,4"
FontWeight="Medium"
Text="Stuff" />
Text="{x:Static lang:Resources.Label_Prompt}" />
<!-- Auto Completion -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Theme}"
IconSource="WeatherMoon">
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="100" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Language}"
IconSource="Character">
Margin="8,0"
Header="{x:Static lang:Resources.Label_AutoCompletion}">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-wand-magic-sparkles" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="100" />
<ToggleSwitch IsChecked="{Binding IsPromptCompletionEnabled}" />
</ui:SettingsExpander.Footer>
<!-- Tag csv selection -->
<ui:SettingsExpanderItem
Content="{x:Static lang:Resources.Label_PromptTags}"
Description="{x:Static lang:Resources.Label_PromptTagsDescription}"
IconSource="Tag"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox ItemsSource="{Binding AvailableTagCompletionCsvs}" SelectedItem="{Binding SelectedTagCompletionCsv}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv import -->
<ui:SettingsExpanderItem
Content="{x:Static lang:Resources.Label_PromptTagsImport}"
IconSource="Add"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding ImportTagCsvCommand}" Content="Import" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Remove underscores -->
<ui:SettingsExpanderItem
Content="{x:Static lang:Resources.Label_CompletionReplaceUnderscoresWithSpaces}"
IconSource="Underline"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsCompletionRemoveUnderscoresEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="3"
Margin="8,0,8,4"
Header="Inference"
IconSource="Code" />
</Grid>
</sg:SpacedGrid>
<!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*,Auto">
<!-- General -->
<sg:SpacedGrid RowDefinitions="Auto,*,*" RowSpacing="4">
<TextBlock
Margin="0,0,0,8"
Margin="0,0,0,4"
FontWeight="Medium"
Text="Other stuff" />
Text="{x:Static lang:Resources.Label_General}" />
<!-- Image Viewer -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
IconSource="Folder">
<ui:SettingsExpander.Footer>
<CheckBox Margin="8" />
</ui:SettingsExpander.Footer>
Header="{x:Static lang:Resources.Label_ImageViewer}"
IconSource="Image"
IsExpanded="True">
<!-- Pixel grid -->
<ui:SettingsExpanderItem Content="Show pixel grid at high zoom levels" IconSource="ViewAll">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsImageViewerPixelGridEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Output Image Files -->
<ui:SettingsExpander
Grid.Row="2"
Header="{x:Static lang:Resources.Label_OutputImageFiles}"
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="TabDesktopImage" />
</ui:SettingsExpander.IconSource>
<!-- File name pattern -->
<ui:SettingsExpanderItem
Content="File name pattern"
Description="{Binding OutputImageFileNameFormatSample}"
IconSource="Rename">
<ui:SettingsExpanderItem.Footer>
<TextBox
Name="OutputImageFileNameFormatTextBox"
MinWidth="150"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
FontSize="13"
Text="{Binding OutputImageFileNameFormat}"
Watermark="{x:Static inference:FileNameFormat.DefaultTemplate}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Grid>
<ui:TeachingTip
Title="Format Variables"
Grid.Row="2"
IsOpen="{Binding #OutputImageFileNameFormatTextBox.IsFocused}"
PreferredPlacement="Top"
Target="{Binding #OutputImageFileNameFormatTextBox, Mode=OneWay}">
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding OutputImageFileNameFormatVars}" />
<!--<mdxaml:MarkdownScrollViewer
Markdown="{Binding OutputImageFileNameFormatGuideMarkdown}"/>-->
</ui:TeachingTip>
</sg:SpacedGrid>
</StackPanel>
</ScrollViewer>
</controls:UserControlBase>

245
StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml

@ -7,6 +7,8 @@
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:hardwareInfo="clr-namespace:StabilityMatrix.Core.Helper.HardwareInfo;assembly=StabilityMatrix.Core"
xmlns:helper="clr-namespace:StabilityMatrix.Core.Helper;assembly=StabilityMatrix.Core"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -28,10 +30,17 @@
<controls:UserControlBase.Resources>
<converters:CultureInfoDisplayConverter x:Key="CultureInfoDisplayConverter" />
<converters:IndexPlusOneConverter x:Key="IndexPlusOneConverter" />
<converters:EnumStringConverter x:Key="EnumStringConverter" />
<converters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</controls:UserControlBase.Resources>
<controls:UserControlBase.Styles>
<Style Selector="sg|SpacedGrid &gt; ui|SettingsExpander">
<Setter Property="Margin" Value="8,0" />
</Style>
</controls:UserControlBase.Styles>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
@ -105,111 +114,29 @@
</ui:SettingsExpander>
</Grid>
<!-- Inference UI -->
<Grid Margin="0,8,0,0" RowDefinitions="auto,*,*,*">
<!-- General -->
<sg:SpacedGrid RowDefinitions="Auto,*" RowSpacing="4">
<TextBlock
Margin="0,0,0,8"
Margin="0,0,0,4"
FontWeight="Medium"
Text="Inference" />
<!-- Auto Completion -->
Text="{x:Static lang:Resources.Label_General}" />
<!-- Link to Inference Sub-Settings -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="Prompt Auto Completion">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-wand-magic-sparkles" />
</ui:SettingsExpander.IconSource>
<!-- Enable toggle -->
<ui:SettingsExpanderItem Content="Enable">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding IsPromptCompletionEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv selection -->
<ui:SettingsExpanderItem
Content="Tag Source"
Description="Tags to use for completion in .csv format (Compatible with a1111-sd-webui-tagcomplete)"
IconSource="Tag"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox ItemsSource="{Binding AvailableTagCompletionCsvs}" SelectedItem="{Binding SelectedTagCompletionCsv}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv import -->
<ui:SettingsExpanderItem
Content="Import Tag Source .csv"
IconSource="Add"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding ImportTagCsvCommand}" Content="Import" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Remove underscores -->
<ui:SettingsExpanderItem
Content="Replace underscores with spaces when inserting completions"
IconSource="Underline"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsCompletionRemoveUnderscoresEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Image Viewer -->
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="Image Viewer"
IconSource="Image">
<!-- Pixel grid -->
<ui:SettingsExpanderItem Content="Show pixel grid at high zoom levels" IconSource="ViewAll">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsImageViewerPixelGridEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Output Image Files -->
<ui:SettingsExpander
Grid.Row="3"
Margin="8,0,8,4"
Header="Output Image Files">
Margin="8,0"
ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:InferenceSettingsViewModel}"
Header="{x:Static lang:Resources.Label_Inference}"
IsClickEnabled="True">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="TabDesktopImage" />
<fluentIcons:SymbolIconSource
FontSize="10"
IsFilled="True"
Symbol="AppGeneric" />
</ui:SettingsExpander.IconSource>
<!-- File name pattern -->
<ui:SettingsExpanderItem
Content="File name pattern"
Description="{Binding OutputImageFileNameFormatSample}"
IconSource="Rename">
<ui:SettingsExpanderItem.Footer>
<TextBox
Name="OutputImageFileNameFormatTextBox"
MinWidth="150"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
FontSize="13"
Text="{Binding OutputImageFileNameFormat}"
Watermark="{x:Static inference:FileNameFormat.DefaultTemplate}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:TeachingTip
Title="Format Variables"
Grid.Row="3"
IsOpen="{Binding #OutputImageFileNameFormatTextBox.IsFocused}"
PreferredPlacement="Top"
Target="{Binding #OutputImageFileNameFormatTextBox, Mode=OneWay}">
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding OutputImageFileNameFormatVars}" />
<!--<mdxaml:MarkdownScrollViewer
Markdown="{Binding OutputImageFileNameFormatGuideMarkdown}"/>-->
</ui:TeachingTip>
</Grid>
</sg:SpacedGrid>
<!-- Environment Options -->
<Grid RowDefinitions="Auto, Auto, Auto">
@ -283,16 +210,15 @@
</sg:SpacedGrid>
<!-- System Options -->
<sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="4">
<sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto,Auto" RowSpacing="4">
<TextBlock
Margin="0,0,0,4"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_System}" />
<!-- Updates page -->
<!-- Updates page -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:UpdateSettingsViewModel}"
@ -308,7 +234,6 @@
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0"
Description="{x:Static lang:Resources.Label_AddToStartMenu_Details}"
Header="{x:Static lang:Resources.Label_AddToStartMenu}"
IconSource="StarAdd"
@ -348,7 +273,6 @@
<ui:SettingsExpander
Grid.Row="3"
Margin="8,0"
Description="{x:Static lang:Resources.Label_SelectNewDataDirectory_Details}"
IconSource="MoveToFolder">
<ui:SettingsExpander.Header>
@ -376,6 +300,121 @@
</Button>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Grid.Row="4" Header="{x:Static lang:Resources.Label_SystemInformation}">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource
FontSize="10"
IsFilled="True"
Symbol="Info" />
</ui:SettingsExpander.IconSource>
<!-- Cpu -->
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.IconSource>
<controls:FASymbolIconSource
FontSize="10"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Symbol="fa-solid fa-microchip" />
</ui:SettingsExpanderItem.IconSource>
<sg:SpacedGrid
DataContext="{Binding CpuInfoAsync^}"
ColumnDefinitions="Auto,Auto"
ColumnSpacing="16"
RowDefinitions="Auto,Auto">
<TextBlock Grid.Column="0" Text="CPU" />
<SelectableTextBlock
Grid.Row="0"
Grid.Column="1"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding ProcessorCaption}"
TextWrapping="WrapWithOverflow" />
</sg:SpacedGrid>
</ui:SettingsExpanderItem>
<!-- Memory -->
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.IconSource>
<controls:FASymbolIconSource
FontSize="10"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Symbol="fa-solid fa-memory" />
</ui:SettingsExpanderItem.IconSource>
<sg:SpacedGrid
DataContext="{Binding MemoryInfo}"
ColumnDefinitions="Auto,Auto"
ColumnSpacing="16"
RowDefinitions="Auto,Auto">
<TextBlock Grid.Column="0" Text="Total Memory" />
<SelectableTextBlock
Grid.Row="0"
Grid.Column="1"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
TextWrapping="WrapWithOverflow">
<SelectableTextBlock.Text>
<MultiBinding StringFormat="{}{0} ({1} usable)">
<Binding Path="TotalInstalledBytes" Converter="{x:Static converters:StringFormatConverters.MemoryBytes}"/>
<Binding Path="TotalPhysicalBytes" Converter="{x:Static converters:StringFormatConverters.MemoryBytes}"/>
</MultiBinding>
</SelectableTextBlock.Text>
</SelectableTextBlock>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Text="Available Memory" />
<SelectableTextBlock
Grid.Row="1"
Grid.Column="1"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding AvailablePhysicalBytes, Converter={x:Static converters:StringFormatConverters.MemoryBytes}}"
TextWrapping="WrapWithOverflow" />
</sg:SpacedGrid>
</ui:SettingsExpanderItem>
<!-- GPUs -->
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.IconSource>
<controls:FASymbolIconSource
FontSize="10"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Symbol="fa-solid fa-tachograph-digital" />
</ui:SettingsExpanderItem.IconSource>
<ItemsControl ItemsSource="{Binding GpuInfos}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="hardwareInfo:GpuInfo">
<sg:SpacedGrid ColumnDefinitions="Auto,Auto,Auto" RowDefinitions="Auto,Auto">
<TextBlock Text="{Binding Index, StringFormat={}{0}, Converter={StaticResource IndexPlusOneConverter}}" Theme="{DynamicResource BodyStrongTextBlockStyle}" />
<SelectableTextBlock
Grid.Row="0"
Grid.Column="1"
Text="{Binding Name}" />
<SelectableTextBlock
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
IsVisible="{Binding !!MemoryBytes}"
Text="{Binding MemoryBytes, Converter={x:Static converters:StringFormatConverters.MemoryBytes}}"
TextWrapping="WrapWithOverflow" />
</sg:SpacedGrid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</sg:SpacedGrid>
<!-- Debug Options -->

3
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

@ -19,10 +19,13 @@
mc:Ignorable="d">
<controls:UserControlBase.Resources>
<!-- Override styles for BreadcrumbBar -->
<!-- ReSharper disable Xaml.RedundantResource -->
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">24</x:Double>
<x:Double x:Key="BreadcrumbBarChevronFontSize">17</x:Double>
<Thickness x:Key="BreadcrumbBarChevronPadding">6,3</Thickness>
<FontWeight x:Key="BreadcrumbBarItemFontWeight">Medium</FontWeight>
<!-- ReSharper restore Xaml.RedundantResource -->
</controls:UserControlBase.Resources>
<Grid RowDefinitions="Auto,*">

46
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -23,8 +23,7 @@ public class LiteDbContext : ILiteDbContext
public event EventHandler? CivitModelsChanged;
// Collections (Tables)
public ILiteCollectionAsync<CivitModel> CivitModels =>
Database.GetCollection<CivitModel>("CivitModels");
public ILiteCollectionAsync<CivitModel> CivitModels => Database.GetCollection<CivitModel>("CivitModels");
public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions =>
Database.GetCollection<CivitModelVersion>("CivitModelVersions");
public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache =>
@ -64,19 +63,12 @@ public class LiteDbContext : ILiteDbContext
{
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync(
new ConnectionString()
{
Filename = dbPath,
Connection = ConnectionType.Shared,
}
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);
}
}
@ -84,30 +76,18 @@ public class LiteDbContext : ILiteDbContext
db ??= new LiteDatabaseAsync(":temp:");
// Register reference fields
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(
m => m.ModelVersions,
"CivitModelVersions"
);
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(
e => e.Items,
"CivitModels"
);
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(m => m.ModelVersions, "CivitModelVersions");
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(e => e.Items, "CivitModels");
return db;
}
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(
string hashBlake3
)
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3)
{
var version = await CivitModelVersions
.Query()
.Where(
mv =>
mv.Files!
.Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3)
mv => mv.Files!.Select(f => f.Hashes).Select(hashes => hashes.BLAKE3).Any(hash => hash == hashBlake3)
)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
@ -128,9 +108,7 @@ public class LiteDbContext : ILiteDbContext
public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel)
{
// Insert model versions first then model
var versionsUpdated = await CivitModelVersions
.UpsertAsync(civitModel.ModelVersions)
.ConfigureAwait(false);
var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions).ConfigureAwait(false);
var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false);
// Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated;
@ -182,8 +160,7 @@ public class LiteDbContext : ILiteDbContext
return null;
}
public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) =>
GithubCache.UpsertAsync(cacheEntry);
public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) => GithubCache.UpsertAsync(cacheEntry);
public void Dispose()
{
@ -194,6 +171,11 @@ public class LiteDbContext : ILiteDbContext
database.Dispose();
}
catch (ObjectDisposedException) { }
catch (ApplicationException)
{
// Ignores a mutex error from library
// https://stability-matrix.sentry.io/share/issue/5c62f37462444e7eab18cea314af231f/
}
database = null;
}

72
StabilityMatrix.Core/Extensions/EnumerableExtensions.cs

@ -2,47 +2,61 @@
public static class EnumerableExtensions
{
public static IEnumerable<(int, T)> Enumerate<T>(
this IEnumerable<T> items,
int start
) {
public static IEnumerable<(int, T)> Enumerate<T>(this IEnumerable<T> items, int start)
{
return items.Select((item, index) => (index + start, item));
}
public static IEnumerable<(int, T)> Enumerate<T>(
this IEnumerable<T> items
) {
public static IEnumerable<(int, T)> Enumerate<T>(this IEnumerable<T> items)
{
return items.Select((item, index) => (index, item));
}
/// <summary>
/// Nested for loop helper
/// </summary>
public static IEnumerable<(T, T)> Product<T>(this IEnumerable<T> items, IEnumerable<T> other)
{
return from item1 in items
from item2 in other
select (item1, item2);
}
return from item1 in items from item2 in other select (item1, item2);
}
public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method,
int concurrency = int.MaxValue)
this IEnumerable<TSource> source,
Func<TSource, Task<TResult>> method,
int concurrency = int.MaxValue
)
{
using var semaphore = new SemaphoreSlim(concurrency);
return await Task.WhenAll(source.Select(async s =>
return await Task.WhenAll(
source.Select(async s =>
{
try
{
// ReSharper disable once AccessToDisposedClosure
await semaphore.WaitAsync().ConfigureAwait(false);
return await method(s).ConfigureAwait(false);
}
finally
{
// ReSharper disable once AccessToDisposedClosure
semaphore.Release();
}
})
)
.ConfigureAwait(false);
}
/// <summary>
/// Executes a specified action on each element in a collection.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <param name="items">The collection to iterate over.</param>
/// <param name="action">The action to perform on each element in the collection.</param>
public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (var item in items)
{
try
{
// ReSharper disable once AccessToDisposedClosure
await semaphore.WaitAsync().ConfigureAwait(false);
return await method(s).ConfigureAwait(false);
}
finally
{
// ReSharper disable once AccessToDisposedClosure
semaphore.Release();
}
})).ConfigureAwait(false);
action(item);
}
}
}

101
StabilityMatrix.Core/Extensions/ObjectExtensions.cs

@ -1,4 +1,5 @@
using System.Reflection;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using JetBrains.Annotations;
using RockLib.Reflection.Optimized;
@ -10,18 +11,17 @@ public static class ObjectExtensions
/// <summary>
/// Cache of Types to named field getters
/// </summary>
private static readonly Dictionary<
Type,
Dictionary<string, Func<object, object>>
> FieldGetterTypeCache = new();
private static readonly Dictionary<Type, Dictionary<string, Func<object, object>>> FieldGetterTypeCache = new();
/// <summary>
/// Cache of Types to named field setters
/// </summary>
private static readonly Dictionary<
Type,
Dictionary<string, Action<object, object>>
> FieldSetterTypeCache = new();
private static readonly Dictionary<Type, Dictionary<string, Action<object, object>>> FieldSetterTypeCache = new();
/// <summary>
/// Cache of Types to named property getters
/// </summary>
private static readonly Dictionary<Type, Dictionary<string, Func<object, object>>> PropertyGetterTypeCache = new();
/// <summary>
/// Get the value of a named private field from an object
@ -38,17 +38,13 @@ public static class ObjectExtensions
if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter))
{
// Get the field
var field = obj.GetType()
.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
// Try get from parent
field ??= obj.GetType()
.BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field is null)
{
throw new ArgumentException(
$"Field {fieldName} not found on type {obj.GetType().Name}"
);
throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}");
}
// Create a getter for the field
@ -61,6 +57,54 @@ public static class ObjectExtensions
return (T?)fieldGetter(obj);
}
/// <summary>
/// Get the value of a protected property from an object
/// </summary>
/// <remarks>
/// The property must be defined by the runtime type of <see cref="obj"/> or its first base type.
/// </remarks>
public static object? GetProtectedProperty(this object obj, [LocalizationRequired(false)] string propertyName)
{
// Check cache
var fieldGetterCache = PropertyGetterTypeCache.GetOrAdd(obj.GetType());
if (!fieldGetterCache.TryGetValue(propertyName, out var propertyGetter))
{
// Get the field
var propertyInfo = obj.GetType()
.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
// Try get from parent
propertyInfo ??= obj.GetType()
.BaseType
?.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (propertyInfo is null)
{
throw new ArgumentException($"Property {propertyName} not found on type {obj.GetType().Name}");
}
// Create a getter for the field
propertyGetter = o => propertyInfo.GetValue(o)!;
// Add to cache
fieldGetterCache.Add(propertyName, propertyGetter);
}
return (object?)propertyGetter(obj);
}
/// <summary>
/// Get the value of a protected property from an object
/// </summary>
/// <remarks>
/// The property must be defined by the runtime type of <see cref="obj"/> or its first base type.
/// </remarks>
public static T? GetProtectedProperty<T>(this object obj, [LocalizationRequired(false)] string propertyName)
where T : class
{
return (T?)GetProtectedProperty(obj, propertyName);
}
/// <summary>
/// Get the value of a named private field from an object
/// </summary>
@ -75,10 +119,7 @@ public static class ObjectExtensions
if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter))
{
// Get the field
var field = typeof(TObject).GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic
);
var field = typeof(TObject).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field is null)
{
@ -108,17 +149,13 @@ public static class ObjectExtensions
if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter))
{
// Get the field
var field = obj.GetType()
.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
// Try get from parent
field ??= obj.GetType()
.BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field is null)
{
throw new ArgumentException(
$"Field {fieldName} not found on type {obj.GetType().Name}"
);
throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}");
}
// Create a setter for the field
@ -142,17 +179,13 @@ public static class ObjectExtensions
if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter))
{
// Get the field
var field = obj.GetType()
.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
// Try get from parent
field ??= obj.GetType()
.BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field is null)
{
throw new ArgumentException(
$"Field {fieldName} not found on type {obj.GetType().Name}"
);
throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}");
}
// Create a setter for the field

73
StabilityMatrix.Core/Helper/ArchiveHelper.cs

@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.RegularExpressions;
using NLog;
@ -59,8 +58,8 @@ public static partial class ArchiveHelper
public static async Task<ArchiveInfo> TestArchive(string archivePath)
{
var process = ProcessRunner.StartAnsiProcess(SevenZipPath, new[] { "t", archivePath });
await process.WaitForExitAsync();
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync().ConfigureAwait(false);
var output = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
var matches = Regex7ZOutput().Matches(output);
var size = ulong.Parse(matches[0].Value);
var compressed = ulong.Parse(matches[1].Value);
@ -86,11 +85,11 @@ public static partial class ArchiveHelper
public static async Task<ArchiveInfo> Extract7Z(string archivePath, string extractDirectory)
{
var args =
$"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y";
var result = await ProcessRunner
.GetProcessResultAsync(
SevenZipPath,
new[] { "x", archivePath, "-o" + ProcessRunner.Quote(extractDirectory), "-y" }
)
.GetProcessResultAsync(SevenZipPath, args)
.ConfigureAwait(false);
result.EnsureSuccessExitCode();
@ -185,7 +184,7 @@ public static partial class ArchiveHelper
throw new ArgumentException("Archive must be a zipped tar.");
}
// Extract the tar.gz to tar
await Extract7Z(archivePath, extractDirectory);
await Extract7Z(archivePath, extractDirectory).ConfigureAwait(false);
// Extract the tar
var tarPath = Path.Combine(extractDirectory, Path.GetFileNameWithoutExtension(archivePath));
@ -196,7 +195,7 @@ public static partial class ArchiveHelper
try
{
return await Extract7Z(tarPath, extractDirectory);
return await Extract7Z(tarPath, extractDirectory).ConfigureAwait(false);
}
finally
{
@ -215,11 +214,11 @@ public static partial class ArchiveHelper
{
if (archivePath.EndsWith(".tar.gz"))
{
return await Extract7ZTar(archivePath, extractDirectory);
return await Extract7ZTar(archivePath, extractDirectory).ConfigureAwait(false);
}
else
{
return await Extract7Z(archivePath, extractDirectory);
return await Extract7Z(archivePath, extractDirectory).ConfigureAwait(false);
}
}
@ -241,7 +240,7 @@ public static partial class ArchiveHelper
var count = 0ul;
// Get true size
var (total, _) = await TestArchive(archivePath);
var (total, _) = await TestArchive(archivePath).ConfigureAwait(false);
// If not available, use the size of the archive file
if (total == 0)
@ -266,32 +265,34 @@ public static partial class ArchiveHelper
};
}
await Task.Factory.StartNew(
() =>
{
var extractOptions = new ExtractionOptions
await Task.Factory
.StartNew(
() =>
{
Overwrite = true,
ExtractFullPath = true,
};
using var stream = File.OpenRead(archivePath);
using var archive = ReaderFactory.Open(stream);
var extractOptions = new ExtractionOptions
{
Overwrite = true,
ExtractFullPath = true,
};
using var stream = File.OpenRead(archivePath);
using var archive = ReaderFactory.Open(stream);
// Start the progress reporting timer
progressMonitor?.Start();
// Start the progress reporting timer
progressMonitor?.Start();
while (archive.MoveToNextEntry())
{
var entry = archive.Entry;
if (!entry.IsDirectory)
while (archive.MoveToNextEntry())
{
count += (ulong)entry.CompressedSize;
var entry = archive.Entry;
if (!entry.IsDirectory)
{
count += (ulong)entry.CompressedSize;
}
archive.WriteEntryToDirectory(outputDirectory, extractOptions);
}
archive.WriteEntryToDirectory(outputDirectory, extractOptions);
}
},
TaskCreationOptions.LongRunning
);
},
TaskCreationOptions.LongRunning
)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(progress: 1, message: "Done extracting"));
progressMonitor?.Stop();
@ -305,7 +306,7 @@ public static partial class ArchiveHelper
public static async Task ExtractManaged(string archivePath, string outputDirectory)
{
await using var stream = File.OpenRead(archivePath);
await ExtractManaged(stream, outputDirectory);
await ExtractManaged(stream, outputDirectory).ConfigureAwait(false);
}
/// <summary>
@ -381,7 +382,7 @@ public static partial class ArchiveHelper
// Write file
await using var entryStream = reader.OpenEntryStream();
await using var fileStream = File.Create(outputPath);
await entryStream.CopyToAsync(fileStream);
await entryStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
}

43
StabilityMatrix.Core/Helper/FileHash.cs

@ -39,10 +39,7 @@ public static class FileHash
}
}
public static async Task<string> GetSha256Async(
string filePath,
IProgress<ProgressReport>? progress = default
)
public static async Task<string> GetSha256Async(string filePath, IProgress<ProgressReport>? progress = default)
{
if (!File.Exists(filePath))
{
@ -62,13 +59,7 @@ public static class FileHash
buffer,
totalBytesRead =>
{
progress?.Report(
new ProgressReport(
totalBytesRead,
totalBytes,
type: ProgressType.Hashing
)
);
progress?.Report(new ProgressReport(totalBytesRead, totalBytes, type: ProgressType.Hashing));
}
)
.ConfigureAwait(false);
@ -80,10 +71,7 @@ public static class FileHash
}
}
public static async Task<string> GetBlake3Async(
string filePath,
IProgress<ProgressReport>? progress = default
)
public static async Task<string> GetBlake3Async(string filePath, IProgress<ProgressReport>? progress = default)
{
if (!File.Exists(filePath))
{
@ -93,7 +81,7 @@ public static class FileHash
var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length);
var readBytes = 0ul;
var shared = ArrayPool<byte>.Shared;
var buffer = shared.Rent((int)FileTransfers.GetBufferSize(totalBytes));
var buffer = shared.Rent(GetBufferSize(totalBytes));
try
{
await using var stream = File.OpenRead(filePath);
@ -117,10 +105,7 @@ public static class FileHash
}
}
public static async Task<Hash> GetBlake3Async(
Stream stream,
IProgress<ProgressReport>? progress = default
)
public static async Task<Hash> GetBlake3Async(Stream stream, IProgress<ProgressReport>? progress = default)
{
var totalBytes = Convert.ToUInt64(stream.Length);
var readBytes = 0ul;
@ -191,11 +176,7 @@ public static class FileHash
false
);
using var accessor = memoryMappedFile.CreateViewAccessor(
0,
totalBytes,
MemoryMappedFileAccess.Read
);
using var accessor = memoryMappedFile.CreateViewAccessor(0, totalBytes, MemoryMappedFileAccess.Read);
Debug.Assert(accessor.Capacity == fileStream.Length);
@ -213,4 +194,16 @@ public static class FileHash
{
return Task.Run(() => GetBlake3MemoryMappedParallel(filePath));
}
/// <summary>
/// Determines suitable buffer size for hashing based on stream length.
/// </summary>
private static int GetBufferSize(ulong totalBytes) =>
totalBytes switch
{
< Size.MiB => 8 * (int)Size.KiB,
< 500 * Size.MiB => 16 * (int)Size.KiB,
< Size.GiB => 32 * (int)Size.KiB,
_ => 64 * (int)Size.KiB
};
}

172
StabilityMatrix.Core/Helper/HardwareHelper.cs

@ -1,172 +0,0 @@
using System.Diagnostics;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace StabilityMatrix.Core.Helper;
public static partial class HardwareHelper
{
private static IReadOnlyList<GpuInfo>? cachedGpuInfos;
private static string RunBashCommand(string command)
{
var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var process = Process.Start(processInfo);
process.WaitForExit();
var output = process.StandardOutput.ReadToEnd();
return output;
}
[SupportedOSPlatform("windows")]
private static IEnumerable<GpuInfo> IterGpuInfoWindows()
{
const string gpuRegistryKeyPath =
@"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}";
using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath);
if (baseKey == null) yield break;
foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0")))
{
using var subKey = baseKey.OpenSubKey(subKeyName);
if (subKey != null)
{
yield return new GpuInfo
{
Name = subKey.GetValue("DriverDesc")?.ToString(),
MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")),
};
}
}
}
[SupportedOSPlatform("linux")]
private static IEnumerable<GpuInfo> IterGpuInfoLinux()
{
var output = RunBashCommand("lspci | grep VGA");
var gpuLines = output.Split("\n");
foreach (var line in gpuLines)
{
if (string.IsNullOrWhiteSpace(line)) continue;
var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line
var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}");
ulong memoryBytes = 0;
string? name = null;
// Parse output with regex
var match = Regex.Match(gpuOutput, @"VGA compatible controller: ([^\n]*)");
if (match.Success)
{
name = match.Groups[1].Value.Trim();
}
match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]");
if (match.Success)
{
memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024;
}
yield return new GpuInfo { Name = name, MemoryBytes = memoryBytes };
}
}
/// <summary>
/// Yields GpuInfo for each GPU in the system.
/// </summary>
public static IEnumerable<GpuInfo> IterGpuInfo()
{
if (Compat.IsWindows)
{
return IterGpuInfoWindows();
}
else if (Compat.IsLinux)
{
// Since this requires shell commands, fetch cached value if available.
if (cachedGpuInfos is not null)
{
return cachedGpuInfos;
}
// No cache, fetch and cache.
cachedGpuInfos = IterGpuInfoLinux().ToList();
return cachedGpuInfos;
}
// TODO: Implement for macOS
return Enumerable.Empty<GpuInfo>();
}
/// <summary>
/// Return true if the system has at least one Nvidia GPU.
/// </summary>
public static bool HasNvidiaGpu()
{
return IterGpuInfo().Any(gpu => gpu.IsNvidia);
}
/// <summary>
/// Return true if the system has at least one AMD GPU.
/// </summary>
public static bool HasAmdGpu()
{
return IterGpuInfo().Any(gpu => gpu.IsAmd);
}
// Set ROCm for default if AMD and Linux
public static bool PreferRocm() => !HasNvidiaGpu()
&& HasAmdGpu()
&& Compat.IsLinux;
// Set DirectML for default if AMD and Windows
public static bool PreferDirectML() => !HasNvidiaGpu()
&& HasAmdGpu()
&& Compat.IsWindows;
}
public enum Level
{
Unknown,
Low,
Medium,
High
}
public record GpuInfo
{
public string? Name { get; init; } = string.Empty;
public ulong MemoryBytes { get; init; }
public Level? MemoryLevel => MemoryBytes switch
{
<= 0 => Level.Unknown,
< 4 * Size.GiB => Level.Low,
< 8 * Size.GiB => Level.Medium,
_ => Level.High
};
public bool IsNvidia
{
get
{
var name = Name?.ToLowerInvariant();
if (string.IsNullOrEmpty(name)) return false;
return name.Contains("nvidia")
|| name.Contains("tesla");
}
}
public bool IsAmd => Name?.ToLowerInvariant().Contains("amd") ?? false;
}

7
StabilityMatrix.Core/Helper/HardwareInfo/CpuInfo.cs

@ -0,0 +1,7 @@
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public readonly record struct CpuInfo
{
public string ProcessorCaption { get; init; }
public string ProcessorName { get; init; }
}

31
StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs

@ -0,0 +1,31 @@
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public record GpuInfo
{
public int Index { get; init; }
public string? Name { get; init; } = string.Empty;
public ulong MemoryBytes { get; init; }
public MemoryLevel? MemoryLevel =>
MemoryBytes switch
{
<= 0 => HardwareInfo.MemoryLevel.Unknown,
< 4 * Size.GiB => HardwareInfo.MemoryLevel.Low,
< 8 * Size.GiB => HardwareInfo.MemoryLevel.Medium,
_ => HardwareInfo.MemoryLevel.High
};
public bool IsNvidia
{
get
{
var name = Name?.ToLowerInvariant();
if (string.IsNullOrEmpty(name))
return false;
return name.Contains("nvidia") || name.Contains("tesla");
}
}
public bool IsAmd => Name?.Contains("amd", StringComparison.OrdinalIgnoreCase) ?? false;
}

234
StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs

@ -0,0 +1,234 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using Hardware.Info;
using Microsoft.Win32;
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public static partial class HardwareHelper
{
private static IReadOnlyList<GpuInfo>? cachedGpuInfos;
private static readonly Lazy<IHardwareInfo> HardwareInfoLazy = new(() => new Hardware.Info.HardwareInfo());
public static IHardwareInfo HardwareInfo => HardwareInfoLazy.Value;
private static string RunBashCommand(string command)
{
var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var process = Process.Start(processInfo);
process.WaitForExit();
var output = process.StandardOutput.ReadToEnd();
return output;
}
[SupportedOSPlatform("windows")]
private static IEnumerable<GpuInfo> IterGpuInfoWindows()
{
const string gpuRegistryKeyPath =
@"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}";
using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath);
if (baseKey == null)
yield break;
var gpuIndex = 0;
foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0")))
{
using var subKey = baseKey.OpenSubKey(subKeyName);
if (subKey != null)
{
yield return new GpuInfo
{
Index = gpuIndex++,
Name = subKey.GetValue("DriverDesc")?.ToString(),
MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")),
};
}
}
}
[SupportedOSPlatform("linux")]
private static IEnumerable<GpuInfo> IterGpuInfoLinux()
{
var output = RunBashCommand("lspci | grep VGA");
var gpuLines = output.Split("\n");
var gpuIndex = 0;
foreach (var line in gpuLines)
{
if (string.IsNullOrWhiteSpace(line))
continue;
var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line
var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}");
ulong memoryBytes = 0;
string? name = null;
// Parse output with regex
var match = Regex.Match(gpuOutput, @"VGA compatible controller: ([^\n]*)");
if (match.Success)
{
name = match.Groups[1].Value.Trim();
}
match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]");
if (match.Success)
{
memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024;
}
yield return new GpuInfo
{
Index = gpuIndex++,
Name = name,
MemoryBytes = memoryBytes
};
}
}
/// <summary>
/// Yields GpuInfo for each GPU in the system.
/// </summary>
public static IEnumerable<GpuInfo> IterGpuInfo()
{
if (Compat.IsWindows)
{
return IterGpuInfoWindows();
}
else if (Compat.IsLinux)
{
// Since this requires shell commands, fetch cached value if available.
if (cachedGpuInfos is not null)
{
return cachedGpuInfos;
}
// No cache, fetch and cache.
cachedGpuInfos = IterGpuInfoLinux().ToList();
return cachedGpuInfos;
}
// TODO: Implement for macOS
return Enumerable.Empty<GpuInfo>();
}
/// <summary>
/// Return true if the system has at least one Nvidia GPU.
/// </summary>
public static bool HasNvidiaGpu()
{
return IterGpuInfo().Any(gpu => gpu.IsNvidia);
}
/// <summary>
/// Return true if the system has at least one AMD GPU.
/// </summary>
public static bool HasAmdGpu()
{
return IterGpuInfo().Any(gpu => gpu.IsAmd);
}
// Set ROCm for default if AMD and Linux
public static bool PreferRocm() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsLinux;
// Set DirectML for default if AMD and Windows
public static bool PreferDirectML() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsWindows;
/// <summary>
/// Gets the total and available physical memory in bytes.
/// </summary>
public static MemoryInfo GetMemoryInfo() =>
Compat.IsWindows ? GetMemoryInfoImplWindows() : GetMemoryInfoImplGeneric();
[SupportedOSPlatform("windows")]
private static MemoryInfo GetMemoryInfoImplWindows()
{
var memoryStatus = new Win32MemoryStatusEx();
if (!GlobalMemoryStatusEx(ref memoryStatus))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (!GetPhysicallyInstalledSystemMemory(out var installedMemoryKb))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return new MemoryInfo
{
TotalInstalledBytes = (ulong)installedMemoryKb * 1024,
TotalPhysicalBytes = memoryStatus.UllTotalPhys,
AvailablePhysicalBytes = memoryStatus.UllAvailPhys
};
}
private static MemoryInfo GetMemoryInfoImplGeneric()
{
HardwareInfo.RefreshMemoryList();
return new MemoryInfo
{
TotalPhysicalBytes = HardwareInfo.MemoryStatus.TotalPhysical,
AvailablePhysicalBytes = HardwareInfo.MemoryStatus.AvailablePhysical
};
}
/// <summary>
/// Gets cpu info
/// </summary>
public static Task<CpuInfo> GetCpuInfoAsync() =>
Compat.IsWindows ? Task.FromResult(GetCpuInfoImplWindows()) : GetCpuInfoImplGenericAsync();
[SupportedOSPlatform("windows")]
private static CpuInfo GetCpuInfoImplWindows()
{
var info = new CpuInfo();
using var processorKey = Registry
.LocalMachine
.OpenSubKey(@"Hardware\Description\System\CentralProcessor\0", RegistryKeyPermissionCheck.ReadSubTree);
if (processorKey?.GetValue("ProcessorNameString") is string processorName)
{
info = info with { ProcessorCaption = processorName.Trim() };
}
return info;
}
private static Task<CpuInfo> GetCpuInfoImplGenericAsync()
{
return Task.Run(() =>
{
HardwareInfo.RefreshCPUList();
return new CpuInfo { ProcessorCaption = HardwareInfo.CpuList.FirstOrDefault()?.Caption.Trim() ?? "" };
});
}
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetPhysicallyInstalledSystemMemory(out long totalMemoryInKilobytes);
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GlobalMemoryStatusEx(ref Win32MemoryStatusEx lpBuffer);
}

10
StabilityMatrix.Core/Helper/HardwareInfo/MemoryInfo.cs

@ -0,0 +1,10 @@
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public readonly record struct MemoryInfo
{
public ulong TotalInstalledBytes { get; init; }
public ulong TotalPhysicalBytes { get; init; }
public ulong AvailablePhysicalBytes { get; init; }
}

9
StabilityMatrix.Core/Helper/HardwareInfo/MemoryLevel.cs

@ -0,0 +1,9 @@
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public enum MemoryLevel
{
Unknown,
Low,
Medium,
High
}

19
StabilityMatrix.Core/Helper/HardwareInfo/Win32MemoryStatusEx.cs

@ -0,0 +1,19 @@
using System.Runtime.InteropServices;
namespace StabilityMatrix.Core.Helper.HardwareInfo;
[StructLayout(LayoutKind.Sequential)]
public struct Win32MemoryStatusEx
{
public uint DwLength = (uint)Marshal.SizeOf(typeof(Win32MemoryStatusEx));
public uint DwMemoryLoad = 0;
public ulong UllTotalPhys = 0;
public ulong UllAvailPhys = 0;
public ulong UllTotalPageFile = 0;
public ulong UllAvailPageFile = 0;
public ulong UllTotalVirtual = 0;
public ulong UllAvailVirtual = 0;
public ulong UllAvailExtendedVirtual = 0;
public Win32MemoryStatusEx() { }
}

46
StabilityMatrix.Core/Helper/ModelFinder.cs

@ -1,4 +1,5 @@
using NLog;
using System.Net;
using NLog;
using Refit;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
@ -8,11 +9,7 @@ using StabilityMatrix.Core.Models.Api;
namespace StabilityMatrix.Core.Helper;
// return Model, ModelVersion, ModelFile
public record struct ModelSearchResult(
CivitModel Model,
CivitModelVersion ModelVersion,
CivitFile ModelFile
);
public record struct ModelSearchResult(CivitModel Model, CivitModelVersion ModelVersion, CivitFile ModelFile);
[Singleton]
public class ModelFinder
@ -36,9 +33,7 @@ public class ModelFinder
return null;
}
var file = version.Files!.First(
file => file.Hashes.BLAKE3?.ToLowerInvariant() == hashBlake3
);
var file = version.Files!.First(file => file.Hashes.BLAKE3?.ToLowerInvariant() == hashBlake3);
return new ModelSearchResult(model, version, file);
}
@ -61,16 +56,39 @@ public class ModelFinder
// VersionResponse is not actually the full data of ModelVersion, so find it again
var version = model.ModelVersions!.First(version => version.Id == versionResponse.Id);
var file = versionResponse.Files.First(
file => file.Hashes.BLAKE3?.ToLowerInvariant() == hashBlake3
);
var file = versionResponse
.Files
.First(file => hashBlake3.Equals(file.Hashes.BLAKE3, StringComparison.OrdinalIgnoreCase));
return new ModelSearchResult(model, version, file);
}
catch (TaskCanceledException e)
{
Logger.Warn(
"Timed out while finding remote model version using hash {Hash}: {Error}",
hashBlake3,
e.Message
);
return null;
}
catch (ApiException e)
{
Logger.Info(
"Could not find remote model version using hash {Hash}: {Error}",
if (e.StatusCode == HttpStatusCode.NotFound)
{
Logger.Info("Could not find remote model version using hash {Hash}", hashBlake3);
}
else
{
Logger.Warn(e, "Could not find remote model version using hash {Hash}: {Error}", hashBlake3, e.Message);
}
return null;
}
catch (HttpRequestException e)
{
Logger.Warn(
e,
"Could not connect to api while finding remote model version using hash {Hash}: {Error}",
hashBlake3,
e.Message
);

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

Loading…
Cancel
Save