diff --git a/CHANGELOG.md b/CHANGELOG.md index 561f363f2..93aa67be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Added base model filter to Checkpoints page - Search box on Checkpoints page now searches tags and trigger words - Added "Compatible Images" category when selecting images for Inference projects +- Added "Find in Model Browser" option to the right-click menu on the Checkpoints page ### Changed - Removed "Failed to load image" notification when loading some images on the Checkpoints page - Installed models will no longer be selectable on the Hugging Face tab of the model browser diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 9688f93d0..94cbef3c9 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -209,6 +209,7 @@ public static void Initialize() PreviewImagePath = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", + UpdateAvailable = true, ConnectedModel = new ConnectedModelInfo { VersionName = "Lightning Auroral", diff --git a/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs index 92bfc564d..49458c6da 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs @@ -43,6 +43,11 @@ public Task RemoveModelAsync(LocalModelFile model) return Task.FromResult(false); } + public Task CheckModelsForUpdates() + { + return Task.CompletedTask; + } + /// public void BackgroundRefreshIndex() { } } diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index df668ab03..2a08ac04f 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -1184,6 +1184,15 @@ public static string Label_FindConnectedMetadata { } } + /// + /// Looks up a localized string similar to Find in Model Browser. + /// + public static string Label_FindInModelBrowser { + get { + return ResourceManager.GetString("Label_FindInModelBrowser", resourceCulture); + } + } + /// /// Looks up a localized string similar to First Page. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index d96411540..701544994 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -927,4 +927,7 @@ Quality + + Find in Model Browser + diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs index a29b2f9d0..e02e7e8a7 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs @@ -18,6 +18,7 @@ using LiteDB; using LiteDB.Async; using NLog; +using OneOf.Types; using Refit; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; @@ -160,6 +161,17 @@ INotificationService notificationService .Where(page => page <= TotalPages && page > 0) .ObserveOn(SynchronizationContext.Current) .Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err)); + + EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; + } + + private void OnNavigateAndFindCivitModelRequested(object? sender, int e) + { + if (e <= 0) + return; + + SearchQuery = $"$#{e}"; + SearchModelsCommand.ExecuteAsync(null).SafeFireAndForget(); } public override void OnLoaded() @@ -399,6 +411,16 @@ private async Task SearchModels() Page = CurrentPageNumber }; + if (SelectedModelType != CivitModelType.All) + { + modelRequest.Types = [SelectedModelType]; + } + + if (SelectedBaseModelType != "All") + { + modelRequest.BaseModel = SelectedBaseModelType; + } + if (SearchQuery.StartsWith("#")) { modelRequest.Tag = SearchQuery[1..]; @@ -407,19 +429,16 @@ private async Task SearchModels() { modelRequest.Username = SearchQuery[1..]; } - else - { - modelRequest.Query = SearchQuery; - } - - if (SelectedModelType != CivitModelType.All) + else if (SearchQuery.StartsWith("$#")) { - modelRequest.Types = [SelectedModelType]; + modelRequest.Period = CivitPeriod.AllTime; + modelRequest.BaseModel = null; + modelRequest.Types = null; + modelRequest.CommaSeparatedModelIds = SearchQuery[2..]; } - - if (SelectedBaseModelType != "All") + else { - modelRequest.BaseModel = SelectedBaseModelType; + modelRequest.Query = SearchQuery; } if (SortMode == CivitSortMode.Installed) diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs index e477851d3..051bd7958 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs @@ -1,13 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using Avalonia; +using System.Threading.Tasks; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; +using FluentAvalonia.Core; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; @@ -15,18 +18,34 @@ namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(CheckpointBrowserPage))] [Singleton] -public partial class CheckpointBrowserViewModel( - CivitAiBrowserViewModel civitAiBrowserViewModel, - HuggingFacePageViewModel huggingFaceViewModel -) : PageViewModelBase +public partial class CheckpointBrowserViewModel : PageViewModelBase { public override string Title => "Model Browser"; - public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true }; + public override IconSource IconSource => + new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true }; - public IReadOnlyList Pages { get; } = - new List( + public IReadOnlyList Pages { get; } + + [ObservableProperty] + private TabItem? selectedPage; + + /// + public CheckpointBrowserViewModel( + CivitAiBrowserViewModel civitAiBrowserViewModel, + HuggingFacePageViewModel huggingFaceViewModel + ) + { + Pages = new List( new List([civitAiBrowserViewModel, huggingFaceViewModel]).Select( vm => new TabItem { Header = vm.Header, Content = vm } ) ); + SelectedPage = Pages.FirstOrDefault(); + EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; + } + + private void OnNavigateAndFindCivitModelRequested(object? sender, int e) + { + SelectedPage = Pages.FirstOrDefault(); + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs index 33ed693d1..f3b817885 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs @@ -63,6 +63,9 @@ public partial class CheckpointFile : ViewModelBase [ObservableProperty] private ProgressReport? progress; + [ObservableProperty] + private bool updateAvailable; + public string FileName => Path.GetFileName(FilePath); public bool CanShowTriggerWords => @@ -212,6 +215,15 @@ private async Task RenameAsync() } } + [RelayCommand] + private void FindOnModelBrowser() + { + if (ConnectedModel?.ModelId == null) + return; + + EventManager.Instance.OnNavigateAndFindCivitModelRequested(ConnectedModel.ModelId); + } + [RelayCommand] private void OpenOnCivitAi() { diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs index ed2a19f44..714d0bd7f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; using System.IO; using System.Linq; @@ -660,12 +659,10 @@ public void Index() SubFoldersCache.EditDiff(updatedFolders, (a, b) => a.Title == b.Title); // Index files + var files = GetCheckpointFiles(); + Dispatcher.UIThread.Post( - () => - { - var files = GetCheckpointFiles(); - checkpointFilesCache.EditDiff(files, CheckpointFile.FilePathComparer); - }, + () => checkpointFilesCache.EditDiff(files, CheckpointFile.FilePathComparer), DispatcherPriority.Background ); } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs index f45db17ff..8639c2104 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Notifications; @@ -153,7 +152,6 @@ ModelFinder modelFinder public override void OnLoaded() { base.OnLoaded(); - var sw = Stopwatch.StartNew(); // Set UI states @@ -173,7 +171,6 @@ public override void OnLoaded() IsLoading = CheckpointFolders.Count == 0; IsIndexing = CheckpointFolders.Count > 0; - // GetStuff(); IndexFolders(); IsLoading = false; IsIndexing = false; diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs index c546d1ee7..76e50c0f6 100644 --- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reactive.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; @@ -11,7 +10,6 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; -using KGySoft.CoreLibraries; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml index a3afe9669..47624c72a 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml @@ -14,5 +14,6 @@ x:DataType="viewModels:CheckpointBrowserViewModel" mc:Ignorable="d" x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage"> - + diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index e8de7da9e..c2831986f 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -27,7 +27,7 @@ Color="#FF000000" Opacity="0.2" x:Key="TextDropShadowEffect" /> - + @@ -70,15 +70,19 @@ IsVisible="{Binding CanShowTriggerWords}" Text="{x:Static lang:Resources.Action_CopyTriggerWords}" IconSource="Copy" /> - + + + IsVisible="{Binding IsConnectedModel}" /> True - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + TextWrapping="WrapWithOverflow" /> + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + FontWeight="Medium" + Text="{x:Static lang:Resources.Label_UpdateAvailable}" /> + + + - - + + + + + + + + + + + + + - - + + - + VerticalAlignment="Center" /> + - + + Margin="0,0,0,8" /> @@ -520,7 +549,7 @@ - + @@ -528,9 +557,8 @@ - - - + + + FontSize="18" /> + VerticalAlignment="Center" /> - + - + Command="{Binding UpdateExistingMetadataCommand}" /> @@ -604,7 +632,7 @@ Title="{x:Static lang:Resources.TeachingTip_MoreCheckpointCategories}" PreferredPlacement="Bottom" IsOpen="{Binding IsCategoryTipOpen}" /> - + + Margin="0,0,0,4" /> + Margin="16,4,16,16" /> logger EventManager.Instance.ToggleProgressFlyout += (_, _) => progressFlyout?.Hide(); EventManager.Instance.CultureChanged += (_, _) => SetDefaultFonts(); EventManager.Instance.UpdateAvailable += OnUpdateAvailable; + EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; Observable .FromEventPattern(this, nameof(SizeChanged)) @@ -138,6 +140,11 @@ ILogger logger }); } + private void OnNavigateAndFindCivitModelRequested(object? sender, int e) + { + navigationService.NavigateTo(); + } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { diff --git a/StabilityMatrix.Core/Helper/EventManager.cs b/StabilityMatrix.Core/Helper/EventManager.cs index 22c09cc24..f77e3047b 100644 --- a/StabilityMatrix.Core/Helper/EventManager.cs +++ b/StabilityMatrix.Core/Helper/EventManager.cs @@ -39,6 +39,7 @@ private EventManager() { } public event EventHandler? InferenceUpscaleRequested; public event EventHandler? InferenceImageToImageRequested; public event EventHandler? InferenceImageToVideoRequested; + public event EventHandler? NavigateAndFindCivitModelRequested; public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress); @@ -86,4 +87,7 @@ public void OnInferenceImageToImageRequested(LocalImageFile imageFile) => public void OnInferenceImageToVideoRequested(LocalImageFile imageFile) => InferenceImageToVideoRequested?.Invoke(this, imageFile); + + public void OnNavigateAndFindCivitModelRequested(int modelId) => + NavigateAndFindCivitModelRequested?.Invoke(this, modelId); } diff --git a/StabilityMatrix.Core/Helper/ModelFinder.cs b/StabilityMatrix.Core/Helper/ModelFinder.cs index f16408215..43517c6af 100644 --- a/StabilityMatrix.Core/Helper/ModelFinder.cs +++ b/StabilityMatrix.Core/Helper/ModelFinder.cs @@ -56,9 +56,9 @@ public ModelFinder(ILiteDbContext liteDbContext, ICivitApi civitApi) // 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 => hashBlake3.Equals(file.Hashes.BLAKE3, StringComparison.OrdinalIgnoreCase)); + var file = versionResponse.Files.First( + file => hashBlake3.Equals(file.Hashes.BLAKE3, StringComparison.OrdinalIgnoreCase) + ); return new ModelSearchResult(model, version, file); } @@ -79,7 +79,12 @@ public ModelFinder(ILiteDbContext liteDbContext, ICivitApi civitApi) } else { - Logger.Warn(e, "Could not find remote model version using hash {Hash}: {Error}", hashBlake3, e.Message); + Logger.Warn( + e, + "Could not find remote model version using hash {Hash}: {Error}", + hashBlake3, + e.Message + ); } return null; @@ -95,4 +100,35 @@ public ModelFinder(ILiteDbContext liteDbContext, ICivitApi civitApi) return null; } } + + public async Task> FindRemoteModelsById(IEnumerable ids) + { + var results = new List(); + + // split ids into batches of 20 + var batches = ids.Select((id, index) => (id, index)) + .GroupBy(tuple => tuple.index / 20) + .Select(group => group.Select(tuple => tuple.id)); + + foreach (var batch in batches) + { + try + { + var response = await civitApi + .GetModels(new CivitModelsRequest { CommaSeparatedModelIds = string.Join(",", batch) }) + .ConfigureAwait(false); + + if (response.Items == null || response.Items.Count == 0) + continue; + + results.AddRange(response.Items); + } + catch (Exception e) + { + Logger.Error("Error while finding remote models by id: {Error}", e.Message); + } + } + + return results; + } } diff --git a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs index 01db400bc..96c3092cd 100644 --- a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs @@ -34,6 +34,16 @@ public record LocalModelFile /// public string? PreviewImageFullPath { get; set; } + /// + /// Whether or not an update is available for this model + /// + public bool HasUpdate { get; set; } + + /// + /// Last time this model was checked for an update + /// + public DateTimeOffset LastUpdateCheck { get; set; } + /// /// File name of the relative path. /// diff --git a/StabilityMatrix.Core/Services/ModelIndexService.cs b/StabilityMatrix.Core/Services/ModelIndexService.cs index bd0182b35..164433543 100644 --- a/StabilityMatrix.Core/Services/ModelIndexService.cs +++ b/StabilityMatrix.Core/Services/ModelIndexService.cs @@ -1,6 +1,4 @@ -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Diagnostics; +using System.Diagnostics; using System.Text; using AsyncAwaitBestPractices; using AutoCtor; @@ -10,6 +8,7 @@ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; @@ -22,6 +21,7 @@ public partial class ModelIndexService : IModelIndexService private readonly ILogger logger; private readonly ISettingsManager settingsManager; private readonly ILiteDbContext liteDbContext; + private readonly ModelFinder modelFinder; public Dictionary> ModelIndex { get; private set; } = new(); @@ -219,4 +219,43 @@ public async Task RemoveModelAsync(LocalModelFile model) return false; } + + // idk do somethin with this + public async Task CheckModelsForUpdateAsync() + { + var installedHashes = settingsManager.Settings.InstalledModelHashes; + var dbModels = ( + await liteDbContext.LocalModelFiles.FindAllAsync().ConfigureAwait(false) + ?? Enumerable.Empty() + ).ToList(); + var ids = dbModels + .Where(x => x.ConnectedModelInfo != null) + .Where( + x => x.LastUpdateCheck == default || x.LastUpdateCheck < DateTimeOffset.UtcNow.AddHours(-8) + ) + .Select(x => x.ConnectedModelInfo!.ModelId); + var remoteModels = (await modelFinder.FindRemoteModelsById(ids).ConfigureAwait(false)).ToList(); + + foreach (var dbModel in dbModels) + { + if (dbModel.ConnectedModelInfo == null) + continue; + + var remoteModel = remoteModels.FirstOrDefault(m => m.Id == dbModel.ConnectedModelInfo!.ModelId); + + var latestVersion = remoteModel?.ModelVersions?.FirstOrDefault(); + var latestHashes = latestVersion + ?.Files + ?.Where(f => f.Type == CivitFileType.Model) + .Select(f => f.Hashes.BLAKE3); + + if (latestHashes == null) + continue; + + dbModel.HasUpdate = !latestHashes.Any(hash => installedHashes?.Contains(hash) ?? false); + dbModel.LastUpdateCheck = DateTimeOffset.UtcNow; + + await liteDbContext.LocalModelFiles.UpsertAsync(dbModel).ConfigureAwait(false); + } + } }