Browse Source

Merge pull request #572 from ionite34/handle-extension-links

handle download links from sm chrome extension & fix model delete not…
pull/629/head
JT 8 months ago committed by GitHub
parent
commit
d01cc2aeb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 6
      StabilityMatrix.Avalonia/App.axaml.cs
  3. 18
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  4. 6
      StabilityMatrix.Avalonia/Languages/Resources.resx
  5. 8
      StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs
  6. 248
      StabilityMatrix.Avalonia/Services/ModelDownloadLinkHandler.cs
  7. 7
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  8. 25
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

3
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

6
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<IDiscordRichPresenceService>(),
provider.GetRequiredService<ServiceManager<ViewModelBase>>(),
provider.GetRequiredService<ITrackedDownloadService>(),
provider.GetRequiredService<IModelIndexService>()
provider.GetRequiredService<IModelIndexService>(),
provider.GetRequiredService<Lazy<IModelDownloadLinkHandler>>()
)
{
Pages =

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

@ -734,6 +734,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Another instance of Stability Matrix is already running. Please close it before starting a new one..
/// </summary>
public static string Label_AnotherInstanceAlreadyRunning {
get {
return ResourceManager.GetString("Label_AnotherInstanceAlreadyRunning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to API Key.
/// </summary>
@ -2273,6 +2282,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Stability Matrix is already running.
/// </summary>
public static string Label_StabilityMatrixAlreadyRunning {
get {
return ResourceManager.GetString("Label_StabilityMatrixAlreadyRunning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Steps.
/// </summary>

6
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -1020,4 +1020,10 @@
<data name="TeachingTip_WebUiButtonMoved" xml:space="preserve">
<value>The 'Open Web UI' button has moved to the command bar</value>
</data>
<data name="Label_AnotherInstanceAlreadyRunning" xml:space="preserve">
<value>Another instance of Stability Matrix is already running. Please close it before starting a new one.</value>
</data>
<data name="Label_StabilityMatrixAlreadyRunning" xml:space="preserve">
<value>Stability Matrix is already running</value>
</data>
</root>

8
StabilityMatrix.Avalonia/Services/IModelDownloadLinkHandler.cs

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Services;
public interface IModelDownloadLinkHandler
{
Task StartListening();
}

248
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<string, Uri> uriHandlerSubscriber,
ILogger<ModelDownloadLinkHandler> 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<CivitFileType>(type, out var civitFileType)
|| !Enum.TryParse<CivitModelFormat>(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<CivitModelFpType>(fp, out var fpType))
{
possibleFiles = possibleFiles?.Where(x => x.Metadata.Fp == fpType);
}
if (!string.IsNullOrWhiteSpace(size) && Enum.TryParse<CivitModelSize>(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<SharedFolderType>().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<FilePath> 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);
}
/// <summary>
/// Saves the preview image to the same directory as the model file
/// </summary>
/// <param name="modelVersion"></param>
/// <param name="modelFilePath"></param>
/// <returns>The file path of the saved preview image</returns>
private async Task<FilePath?> 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;
}
}

7
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<ISettingsManager>();
settingsManager.Transaction(s =>
{
s.InstalledModelHashes?.Remove(ConnectedModel.Hashes.BLAKE3);
});
}
}
catch (IOException ex)

25
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<IModelDownloadLinkHandler> modelDownloadLinkHandler;
public string Greeting => "Welcome to Avalonia!";
[ObservableProperty]
@ -71,7 +73,8 @@ public partial class MainWindowViewModel : ViewModelBase
IDiscordRichPresenceService discordRichPresenceService,
ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService
IModelIndexService modelIndexService,
Lazy<IModelDownloadLinkHandler> 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<ProgressManagerViewModel>();
UpdateViewModel = dialogFactory.Get<UpdateViewModel>();
}
@ -107,6 +111,25 @@ public partial class MainWindowViewModel : ViewModelBase
return;
}
try
{
await modelDownloadLinkHandler.Value.StartListening();
}
catch (IOException)
{
var dialog = new BetterContentDialog
{
Title = Resources.Label_StabilityMatrixAlreadyRunning,
Content = Resources.Label_AnotherInstanceAlreadyRunning,
IsPrimaryButtonEnabled = true,
PrimaryButtonText = Resources.Action_Close,
DefaultButton = ContentDialogButton.Primary
};
await dialog.ShowAsync();
App.Shutdown();
return;
}
// Initialize Discord Rich Presence (this needs LibraryDir so is set here)
discordRichPresenceService.UpdateState();

Loading…
Cancel
Save