From a5beaf434ced3b8240fb6e984a3519d9c660b628 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 14 Mar 2024 22:06:38 -0700 Subject: [PATCH 1/3] handle download links from sm chrome extension & fix model delete not deleting from cache --- CHANGELOG.md | 3 + .../Services/IModelDownloadLinkHandler.cs | 8 + .../Services/ModelDownloadLinkHandler.cs | 248 ++++++++++++++++++ .../CivitAiBrowserViewModel.cs | 5 +- .../CheckpointManager/CheckpointFile.cs | 7 + 5 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs create mode 100644 StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a94ec3..0a543042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ 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.10.0-dev.3 +### Added +- Added support for deep links from the new Stability Matrix Chrome extension ### Fixed - Fixed Inference HiresFix module "Inherit Primary Sampler Addons" setting not effectively disabling when unchecked - Fixed model download location options for VAEs in the CivitAI Model Browser @@ -13,6 +15,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Fixed One-Click install progress dialog not disappearing after completion - Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area - Fixed no packages being available for one-click install on PCs without a GPU +- Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page ## v2.10.0-dev.2 ### Added diff --git a/StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs b/StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs new file mode 100644 index 00000000..dfea3c3f --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace StabilityMatrix.Avalonia.Services; + +public interface IModelDownloadLinkHandler +{ + Task StartListening(); +} diff --git a/StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs b/StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs new file mode 100644 index 00000000..326d715c --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs @@ -0,0 +1,248 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using Avalonia.Controls.Notifications; +using Avalonia.Threading; +using MessagePipe; +using Microsoft.Extensions.Logging; +using StabilityMatrix.Avalonia.Helpers; +using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.Services; + +[Singleton(typeof(IModelDownloadLinkHandler)), Singleton(typeof(IAsyncDisposable))] +public class ModelDownloadLinkHandler( + IDistributedSubscriber uriHandlerSubscriber, + ILogger logger, + ICivitApi civitApi, + INotificationService notificationService, + ISettingsManager settingsManager, + IDownloadService downloadService, + ITrackedDownloadService trackedDownloadService +) : IAsyncDisposable, IModelDownloadLinkHandler +{ + private IAsyncDisposable? uriHandlerSubscription; + private const string DownloadCivitModel = "downloadCivitModel"; + + public async Task StartListening() + { + uriHandlerSubscription = await uriHandlerSubscriber.SubscribeAsync( + UriHandler.IpcKeySend, + UriReceivedHandler + ); + } + + public async ValueTask DisposeAsync() + { + if (uriHandlerSubscription is not null) + { + await uriHandlerSubscription.DisposeAsync(); + uriHandlerSubscription = null; + } + } + + private void UriReceivedHandler(Uri receivedUri) + { + logger.LogDebug("ModelDownloadLinkHandler Received URI: {Uri}", receivedUri.PathAndQuery); + if (!receivedUri.Host.Equals(DownloadCivitModel, StringComparison.OrdinalIgnoreCase)) + return; + + var queryDict = HttpUtility.ParseQueryString(receivedUri.Query); + var modelIdStr = queryDict["modelId"]; + var modelVersionIdStr = queryDict["modelVersionId"]; + var type = queryDict["type"]; + var format = queryDict["format"]; + var size = queryDict["size"]; + var fp = queryDict["fp"]; + + if ( + string.IsNullOrWhiteSpace(modelIdStr) + || string.IsNullOrWhiteSpace(type) + || string.IsNullOrWhiteSpace(format) + || !int.TryParse(modelIdStr, out var modelId) + || !Enum.TryParse(type, out var civitFileType) + || !Enum.TryParse(format, out var civitFormat) + ) + { + logger.LogError("ModelDownloadLinkHandler: Invalid query parameters"); + + Dispatcher.UIThread.Post( + () => + notificationService.Show( + new Notification( + "Invalid Download Link", + "The download link is invalid", + NotificationType.Error + ) + ) + ); + return; + } + + Dispatcher.UIThread.Post( + () => + notificationService.Show( + "Link Received", + "Successfully received download link", + NotificationType.Warning + ) + ); + + var modelTask = civitApi.GetModelById(modelId); + modelTask.Wait(); + var model = modelTask.Result; + + var useModelVersion = !string.IsNullOrWhiteSpace(modelVersionIdStr); + var modelVersionId = useModelVersion ? int.Parse(modelVersionIdStr) : 0; + + var modelVersion = useModelVersion + ? model.ModelVersions?.FirstOrDefault(x => x.Id == modelVersionId) + : model.ModelVersions?.FirstOrDefault(); + + if (modelVersion is null) + { + logger.LogError("ModelDownloadLinkHandler: Model version not found"); + Dispatcher.UIThread.Post( + () => + notificationService.Show( + new Notification( + "Model has no versions available", + "This model has no versions available for download", + NotificationType.Error + ) + ) + ); + return; + } + + var possibleFiles = modelVersion.Files?.Where( + x => x.Type == civitFileType && x.Metadata.Format == civitFormat + ); + + if (!string.IsNullOrWhiteSpace(fp) && Enum.TryParse(fp, out var fpType)) + { + possibleFiles = possibleFiles?.Where(x => x.Metadata.Fp == fpType); + } + + if (!string.IsNullOrWhiteSpace(size) && Enum.TryParse(size, out var modelSize)) + { + possibleFiles = possibleFiles?.Where(x => x.Metadata.Size == modelSize); + } + + possibleFiles = possibleFiles?.ToList(); + + if (possibleFiles is null) + { + Dispatcher.UIThread.Post( + () => + notificationService.Show( + new Notification( + "Model has no files available", + "This model has no files available for download", + NotificationType.Error + ) + ) + ); + logger.LogError("ModelDownloadLinkHandler: Model file not found"); + return; + } + + var selectedFile = possibleFiles.FirstOrDefault() ?? modelVersion.Files?.FirstOrDefault(); + + var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); + var downloadDirectory = rootModelsDirectory.JoinDir( + selectedFile.Type == CivitFileType.VAE + ? SharedFolderType.VAE.GetStringValue() + : model.Type.ConvertTo().GetStringValue() + ); + + downloadDirectory.Create(); + var downloadPath = downloadDirectory.JoinFile(selectedFile.Name); + + // Create tracked download + var download = trackedDownloadService.NewDownload(selectedFile.DownloadUrl, downloadPath); + + // Download model info and preview first + var saveCmInfoTask = SaveCmInfo(model, modelVersion, selectedFile, downloadDirectory); + var savePreviewImageTask = SavePreviewImage(modelVersion, downloadPath); + + Task.WaitAll([saveCmInfoTask, savePreviewImageTask]); + + var cmInfoPath = saveCmInfoTask.Result; + var previewImagePath = savePreviewImageTask.Result; + + // Add hash info + download.ExpectedHashSha256 = selectedFile.Hashes.SHA256; + + // Add files to cleanup list + download.ExtraCleanupFileNames.Add(cmInfoPath); + if (previewImagePath is not null) + { + download.ExtraCleanupFileNames.Add(previewImagePath); + } + + // Add hash context action + download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(selectedFile); + + download.Start(); + + Dispatcher.UIThread.Post( + () => notificationService.Show("Download Started", $"Downloading {selectedFile.Name}") + ); + } + + private static async Task SaveCmInfo( + CivitModel model, + CivitModelVersion modelVersion, + CivitFile modelFile, + DirectoryPath downloadDirectory + ) + { + var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name); + var modelInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow); + + await modelInfo.SaveJsonToDirectory(downloadDirectory, modelFileName); + + var jsonName = $"{modelFileName}.cm-info.json"; + return downloadDirectory.JoinFile(jsonName); + } + + /// + /// Saves the preview image to the same directory as the model file + /// + /// + /// + /// The file path of the saved preview image + private async Task SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath) + { + // Skip if model has no images + if (modelVersion.Images == null || modelVersion.Images.Count == 0) + { + return null; + } + + var image = modelVersion.Images[0]; + var imageExtension = Path.GetExtension(image.Url).TrimStart('.'); + if (imageExtension is "jpg" or "jpeg" or "png") + { + var imageDownloadPath = modelFilePath.Directory!.JoinFile( + $"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}" + ); + + var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); + await notificationService.TryAsync(imageTask, "Could not download preview image"); + + return imageDownloadPath; + } + + return null; + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs index 924d17ed..a1ffd4e4 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs @@ -139,7 +139,8 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase ISettingsManager settingsManager, ServiceManager dialogFactory, ILiteDbContext liteDbContext, - INotificationService notificationService + INotificationService notificationService, + IModelDownloadLinkHandler modelDownloadLinkHandler ) { this.civitApi = civitApi; @@ -163,6 +164,8 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase .Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err)); EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; + + modelDownloadLinkHandler.StartListening().SafeFireAndForget(); } private void OnNavigateAndFindCivitModelRequested(object? sender, int e) diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs index c7398817..baebb0fd 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs @@ -8,6 +8,7 @@ using Avalonia.Data; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; +using Microsoft.Extensions.DependencyInjection; using NLog; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -136,6 +137,12 @@ public partial class CheckpointFile : ViewModelBase { await Task.Run(() => File.Delete(cmInfoPath)); } + + var settingsManager = App.Services.GetRequiredService(); + settingsManager.Transaction(s => + { + s.InstalledModelHashes?.Remove(ConnectedModel.Hashes.BLAKE3); + }); } } catch (IOException ex) From 86bcdaf86ff3ff733d85b99e5b20078595bef7f5 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 15 Mar 2024 17:26:02 -0700 Subject: [PATCH 2/3] show dialog if another instance is running & fix design view hogging the pipe --- StabilityMatrix.Avalonia/App.axaml.cs | 6 ++--- .../CivitAiBrowserViewModel.cs | 5 +--- .../ViewModels/MainWindowViewModel.cs | 26 ++++++++++++++++++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 014d25d8..71af8fde 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -40,7 +40,6 @@ using Polly.Extensions.Http; using Polly.Timeout; using Refit; using Sentry; -using StabilityMatrix.Avalonia.DesignData; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; @@ -54,14 +53,12 @@ using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; -using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Settings; 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; @@ -323,7 +320,8 @@ public sealed class App : Application provider.GetRequiredService(), provider.GetRequiredService>(), provider.GetRequiredService(), - provider.GetRequiredService() + provider.GetRequiredService(), + provider.GetRequiredService>() ) { Pages = diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs index a1ffd4e4..924d17ed 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs @@ -139,8 +139,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase ISettingsManager settingsManager, ServiceManager dialogFactory, ILiteDbContext liteDbContext, - INotificationService notificationService, - IModelDownloadLinkHandler modelDownloadLinkHandler + INotificationService notificationService ) { this.civitApi = civitApi; @@ -164,8 +163,6 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase .Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err)); EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; - - modelDownloadLinkHandler.StartListening().SafeFireAndForget(); } private void OnNavigateAndFindCivitModelRequested(object? sender, int e) diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs index 718cd574..b6d85ad3 100644 --- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; @@ -36,6 +37,7 @@ public partial class MainWindowViewModel : ViewModelBase private readonly ITrackedDownloadService trackedDownloadService; private readonly IDiscordRichPresenceService discordRichPresenceService; private readonly IModelIndexService modelIndexService; + private readonly Lazy modelDownloadLinkHandler; public string Greeting => "Welcome to Avalonia!"; [ObservableProperty] @@ -71,7 +73,8 @@ public partial class MainWindowViewModel : ViewModelBase IDiscordRichPresenceService discordRichPresenceService, ServiceManager dialogFactory, ITrackedDownloadService trackedDownloadService, - IModelIndexService modelIndexService + IModelIndexService modelIndexService, + Lazy modelDownloadLinkHandler ) { this.settingsManager = settingsManager; @@ -79,6 +82,7 @@ public partial class MainWindowViewModel : ViewModelBase this.discordRichPresenceService = discordRichPresenceService; this.trackedDownloadService = trackedDownloadService; this.modelIndexService = modelIndexService; + this.modelDownloadLinkHandler = modelDownloadLinkHandler; ProgressManagerViewModel = dialogFactory.Get(); UpdateViewModel = dialogFactory.Get(); } @@ -107,6 +111,26 @@ public partial class MainWindowViewModel : ViewModelBase return; } + try + { + await modelDownloadLinkHandler.Value.StartListening(); + } + catch (IOException) + { + var dialog = new BetterContentDialog + { + Title = "Stability Matrix is already running", + Content = + "Another instance of Stability Matrix is already running. Please close it before starting a new one.", + IsPrimaryButtonEnabled = true, + PrimaryButtonText = "Close", + DefaultButton = ContentDialogButton.Primary + }; + await dialog.ShowAsync(); + App.Shutdown(); + return; + } + // Initialize Discord Rich Presence (this needs LibraryDir so is set here) discordRichPresenceService.UpdateState(); From ec4c7343da0b58e972b8dbedf993a0533d620b15 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 15 Mar 2024 17:27:34 -0700 Subject: [PATCH 3/3] i18n --- .../Languages/Resources.Designer.cs | 18 ++++++++++++++++++ .../Languages/Resources.resx | 6 ++++++ .../ViewModels/MainWindowViewModel.cs | 7 +++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index dd2b597d..eb621b57 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -734,6 +734,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Another instance of Stability Matrix is already running. Please close it before starting a new one.. + /// + public static string Label_AnotherInstanceAlreadyRunning { + get { + return ResourceManager.GetString("Label_AnotherInstanceAlreadyRunning", resourceCulture); + } + } + /// /// Looks up a localized string similar to API Key. /// @@ -2273,6 +2282,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Stability Matrix is already running. + /// + public static string Label_StabilityMatrixAlreadyRunning { + get { + return ResourceManager.GetString("Label_StabilityMatrixAlreadyRunning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Steps. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 9f35cbfa..6b0bde49 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -1020,4 +1020,10 @@ The 'Open Web UI' button has moved to the command bar + + Another instance of Stability Matrix is already running. Please close it before starting a new one. + + + Stability Matrix is already running + diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs index b6d85ad3..2b77217a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs @@ -119,11 +119,10 @@ public partial class MainWindowViewModel : ViewModelBase { var dialog = new BetterContentDialog { - Title = "Stability Matrix is already running", - Content = - "Another instance of Stability Matrix is already running. Please close it before starting a new one.", + Title = Resources.Label_StabilityMatrixAlreadyRunning, + Content = Resources.Label_AnotherInstanceAlreadyRunning, IsPrimaryButtonEnabled = true, - PrimaryButtonText = "Close", + PrimaryButtonText = Resources.Action_Close, DefaultButton = ContentDialogButton.Primary }; await dialog.ShowAsync();