diff --git a/StabilityMatrix/App.xaml.cs b/StabilityMatrix/App.xaml.cs
index 64039843..154edff6 100644
--- a/StabilityMatrix/App.xaml.cs
+++ b/StabilityMatrix/App.xaml.cs
@@ -18,7 +18,6 @@ using Microsoft.Extensions.Logging;
using NLog;
using NLog.Config;
using NLog.Extensions.Logging;
-using NLog.Layouts;
using NLog.Targets;
using Octokit;
using Polly;
@@ -361,9 +360,13 @@ namespace StabilityMatrix
};
var exceptionWindow = new ExceptionWindow
{
- DataContext = vm,
- Owner = MainWindow
+ DataContext = vm
};
+
+ if (MainWindow?.IsActive ?? false)
+ {
+ exceptionWindow.Owner = MainWindow;
+ }
exceptionWindow.ShowDialog();
}
e.Handled = true;
diff --git a/StabilityMatrix/CheckpointBrowserPage.xaml b/StabilityMatrix/CheckpointBrowserPage.xaml
index cd6513d2..3a784e6e 100644
--- a/StabilityMatrix/CheckpointBrowserPage.xaml
+++ b/StabilityMatrix/CheckpointBrowserPage.xaml
@@ -139,7 +139,7 @@
+
+
: JsonConverter where T : Enum
+{
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ {
+ throw new JsonException();
+ }
+
+ var enumText = reader.GetString();
+ if (Enum.TryParse(typeof(T), enumText, true, out var result))
+ {
+ return (T) result!;
+ }
+
+ // Unknown value handling
+ if (Enum.TryParse(typeof(T), "Unknown", true, out var unknownResult))
+ {
+ return (T) unknownResult!;
+ }
+
+ throw new JsonException($"Unable to parse '{enumText}' to enum '{typeof(T)}'.");
+ }
+
+ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
+ {
+ if (value == null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+ writer.WriteStringValue(value.GetStringValue());
+ }
+}
diff --git a/StabilityMatrix/Helper/IPrerequisiteHelper.cs b/StabilityMatrix/Helper/IPrerequisiteHelper.cs
index 1bb577ad..6dc948c9 100644
--- a/StabilityMatrix/Helper/IPrerequisiteHelper.cs
+++ b/StabilityMatrix/Helper/IPrerequisiteHelper.cs
@@ -8,10 +8,13 @@ public interface IPrerequisiteHelper
{
string GitBinPath { get; }
+ bool IsPythonInstalled { get; }
+
Task InstallAllIfNecessary(IProgress? progress = null);
Task UnpackResourcesIfNecessary(IProgress? progress = null);
Task InstallGitIfNecessary(IProgress? progress = null);
Task InstallVcRedistIfNecessary(IProgress? progress = null);
+ Task InstallPythonIfNecessary(IProgress? progress = null);
///
/// Run embedded git with the given arguments.
diff --git a/StabilityMatrix/Helper/PrerequisiteHelper.cs b/StabilityMatrix/Helper/PrerequisiteHelper.cs
index f4e0a8a2..e317ad65 100644
--- a/StabilityMatrix/Helper/PrerequisiteHelper.cs
+++ b/StabilityMatrix/Helper/PrerequisiteHelper.cs
@@ -42,6 +42,8 @@ public class PrerequisiteHelper : IPrerequisiteHelper
private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.exe");
private string GitExePath => Path.Combine(PortableGitInstallDir, "bin", "git.exe");
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin");
+
+ public bool IsPythonInstalled => File.Exists(PythonDllPath);
public PrerequisiteHelper(ILogger logger, IGitHubClient gitHubClient,
IDownloadService downloadService, ISettingsManager settingsManager)
@@ -215,9 +217,8 @@ public class PrerequisiteHelper : IPrerequisiteHelper
logger.LogInformation("Git not found at {GitExePath}, downloading...", GitExePath);
- var latestRelease = await gitHubClient.Repository.Release.GetLatest("git-for-windows", "git");
- var portableGitUrl = latestRelease.Assets
- .First(a => a.Name.EndsWith("64-bit.7z.exe")).BrowserDownloadUrl;
+ var portableGitUrl =
+ "https://github.com/git-for-windows/git/releases/download/v2.41.0.windows.1/PortableGit-2.41.0-64-bit.7z.exe";
if (!File.Exists(PortableGitDownloadPath))
{
diff --git a/StabilityMatrix/Models/Api/CivitModelType.cs b/StabilityMatrix/Models/Api/CivitModelType.cs
index e397cf23..29c9a169 100644
--- a/StabilityMatrix/Models/Api/CivitModelType.cs
+++ b/StabilityMatrix/Models/Api/CivitModelType.cs
@@ -1,13 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
+using StabilityMatrix.Converters.Json;
using StabilityMatrix.Extensions;
namespace StabilityMatrix.Models.Api;
-[JsonConverter(typeof(JsonStringEnumConverter))]
+[JsonConverter(typeof(DefaultUnknownEnumConverter))]
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum CivitModelType
{
+ Unknown,
[ConvertTo(SharedFolderType.StableDiffusion)]
Checkpoint,
[ConvertTo(SharedFolderType.TextualInversion)]
diff --git a/StabilityMatrix/SettingsPage.xaml b/StabilityMatrix/SettingsPage.xaml
index 005bcd17..a3505e90 100644
--- a/StabilityMatrix/SettingsPage.xaml
+++ b/StabilityMatrix/SettingsPage.xaml
@@ -133,10 +133,25 @@
FontWeight="Bold"
Margin="0,8"
Text="Embedded Python" />
-
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix/ViewModels/CheckpointBrowserViewModel.cs b/StabilityMatrix/ViewModels/CheckpointBrowserViewModel.cs
index 56b4ef1c..5b7a0591 100644
--- a/StabilityMatrix/ViewModels/CheckpointBrowserViewModel.cs
+++ b/StabilityMatrix/ViewModels/CheckpointBrowserViewModel.cs
@@ -46,13 +46,15 @@ public partial class CheckpointBrowserViewModel : ObservableObject
[ObservableProperty] private bool canGoToNextPage;
[ObservableProperty] private bool canGoToPreviousPage;
[ObservableProperty] private bool isIndeterminate;
+ [ObservableProperty] private bool noResultsFound;
+ [ObservableProperty] private string noResultsText;
public IEnumerable AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast();
public IEnumerable AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast();
public IEnumerable AllModelTypes => Enum.GetValues(typeof(CivitModelType))
.Cast()
- .Where(t => t != CivitModelType.AestheticGradient && t != CivitModelType.Poses)
+ .Where(t => t == CivitModelType.All || t.ConvertTo() > 0)
.OrderBy(t => t.ToString());
public CheckpointBrowserViewModel(
@@ -100,10 +102,25 @@ public partial class CheckpointBrowserViewModel : ObservableObject
var models = modelsResponse.Items;
if (models is null)
{
- Logger.Debug("CivitAI Query {Text} returned no results (in {Elapsed:F1} s)", queryText, timer.Elapsed.TotalSeconds);
+ Logger.Debug("CivitAI Query {Text} returned no results (in {Elapsed:F1} s)",
+ queryText, timer.Elapsed.TotalSeconds);
return;
}
- Logger.Debug("CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)", queryText, models.Count, timer.Elapsed.TotalSeconds);
+
+ Logger.Debug("CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)",
+ queryText, models.Count, timer.Elapsed.TotalSeconds);
+
+ var unknown = models.Where(m => m.Type == CivitModelType.Unknown).ToList();
+ if (unknown.Any())
+ {
+ var names = unknown.Select(m => m.Name).ToList();
+ Logger.Warn("Excluded {Unknown} unknown model types: {Models}", unknown.Count,
+ names);
+ }
+
+ // Filter out unknown model types
+ models = models.Where(m => m.Type.ConvertTo() > 0).ToList();
+
// Database update calls will invoke `OnModelsUpdated`
// Add to database
await liteDbContext.UpsertCivitModelAsync(models);
@@ -116,7 +133,7 @@ public partial class CheckpointBrowserViewModel : ObservableObject
Items = models,
Metadata = modelsResponse.Metadata
});
-
+
if (cacheNew)
{
Logger.Debug("New cache entry, updating model cards");
@@ -133,6 +150,11 @@ public partial class CheckpointBrowserViewModel : ObservableObject
"CivitAI can't be reached right now").SafeFireAndForget();
Logger.Log(LogLevel.Error, e);
}
+ finally
+ {
+ ShowMainLoadingSpinner = false;
+ UpdateResultsText();
+ }
}
///
@@ -160,23 +182,43 @@ public partial class CheckpointBrowserViewModel : ObservableObject
HasSearched = true;
}
+ private string previousSearchQuery = string.Empty;
+
[RelayCommand]
private async Task SearchModels()
{
if (string.IsNullOrWhiteSpace(SearchQuery)) return;
-
var timer = Stopwatch.StartNew();
-
+
+ if (SearchQuery != previousSearchQuery)
+ {
+ // Reset page number
+ CurrentPageNumber = 1;
+ previousSearchQuery = SearchQuery;
+ }
+
// Build request
var modelRequest = new CivitModelsRequest
{
- Query = SearchQuery,
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)
{
@@ -189,7 +231,7 @@ public partial class CheckpointBrowserViewModel : ObservableObject
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
// If cached, update model cards
- if (cachedQuery?.Items is not null && cachedQuery.Items.Any())
+ if (cachedQuery is not null)
{
var elapsed = timer.Elapsed;
Logger.Debug("Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)",
@@ -199,14 +241,13 @@ public partial class CheckpointBrowserViewModel : ObservableObject
// 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)
+ if (timeSinceCache?.TotalMinutes >= 2)
{
- Logger.Debug("Cached query was less than 2 minutes ago ({Seconds:F0} s), skipping remote query",
+ CivitModelQuery(modelRequest).SafeFireAndForget();
+ Logger.Debug(
+ "Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query",
timeSinceCache.Value.TotalSeconds);
- return;
}
-
- CivitModelQuery(modelRequest).SafeFireAndForget();
}
else
{
@@ -214,6 +255,8 @@ public partial class CheckpointBrowserViewModel : ObservableObject
ShowMainLoadingSpinner = true;
await CivitModelQuery(modelRequest);
}
+
+ UpdateResultsText();
}
[RelayCommand]
@@ -251,6 +294,11 @@ public partial class CheckpointBrowserViewModel : ObservableObject
{
settingsManager.SetModelBrowserNsfwEnabled(value);
ModelCardsView?.Refresh();
+
+ if (!HasSearched)
+ return;
+
+ UpdateResultsText();
}
partial void OnSelectedPeriodChanged(CivitPeriod oldValue, CivitPeriod newValue)
@@ -281,4 +329,12 @@ public partial class CheckpointBrowserViewModel : ObservableObject
// execute command instead of calling method directly so that the IsRunning property gets updated
await SearchModelsCommand.ExecuteAsync(null);
}
+
+ private void UpdateResultsText()
+ {
+ NoResultsFound = ModelCardsView?.IsEmpty ?? true;
+ NoResultsText = ModelCards?.Count > 0
+ ? $"{ModelCards.Count} results hidden by filters"
+ : "No results found";
+ }
}
diff --git a/StabilityMatrix/ViewModels/SettingsViewModel.cs b/StabilityMatrix/ViewModels/SettingsViewModel.cs
index 6dd06d5c..03b22c8d 100644
--- a/StabilityMatrix/ViewModels/SettingsViewModel.cs
+++ b/StabilityMatrix/ViewModels/SettingsViewModel.cs
@@ -41,6 +41,7 @@ public partial class SettingsViewModel : ObservableObject
private readonly IPyRunner pyRunner;
private readonly ISnackbarService snackbarService;
private readonly ILiteDbContext liteDbContext;
+ private readonly IPrerequisiteHelper prerequisiteHelper;
private static string LicensesPath => "pack://application:,,,/Assets/licenses.json";
public TextToFlowDocumentConverter? TextToFlowDocumentConverter { get; set; }
@@ -62,6 +63,8 @@ public partial class SettingsViewModel : ObservableObject
[ObservableProperty] private bool isFileSearchFlyoutOpen;
[ObservableProperty] private double fileSearchProgress;
+ [ObservableProperty] private bool isPythonInstalling;
+
[ObservableProperty] private string? webApiHost;
[ObservableProperty] private string? webApiPort;
[ObservableProperty] private string? webApiActivePackageHost;
@@ -94,7 +97,8 @@ public partial class SettingsViewModel : ObservableObject
ISnackbarService snackbarService,
ILogger logger,
IPackageFactory packageFactory,
- ILiteDbContext liteDbContext)
+ ILiteDbContext liteDbContext,
+ IPrerequisiteHelper prerequisiteHelper)
{
this.logger = logger;
this.settingsManager = settingsManager;
@@ -104,6 +108,7 @@ public partial class SettingsViewModel : ObservableObject
this.a3WebApiManager = a3WebApiManager;
this.pyRunner = pyRunner;
this.liteDbContext = liteDbContext;
+ this.prerequisiteHelper = prerequisiteHelper;
SelectedTheme = settingsManager.Settings.Theme ?? "Dark";
WindowBackdropType = settingsManager.Settings.WindowBackdropType ?? WindowBackdropType.Mica;
}
@@ -169,6 +174,16 @@ public partial class SettingsViewModel : ObservableObject
public AsyncRelayCommand PythonVersionCommand => new(async () =>
{
+ // Ensure python installed
+ if (!prerequisiteHelper.IsPythonInstalled)
+ {
+ IsPythonInstalling = true;
+ // Need 7z as well for site packages repack
+ await prerequisiteHelper.UnpackResourcesIfNecessary();
+ await prerequisiteHelper.InstallPythonIfNecessary();
+ IsPythonInstalling = false;
+ }
+
// Get python version
await pyRunner.Initialize();
var result = await pyRunner.GetVersionInfo();