diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a543042..c5e42023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 ## v2.10.0-dev.3 ### Added - Added support for deep links from the new Stability Matrix Chrome extension +### Changed +- Due to changes on the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser +- Due to the above API changes, new pages are now loaded via "infinite scrolling" ### 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 @@ -16,6 +19,9 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - 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 +- Fixed missing ratings on some models in the CivitAI Model Browser +- Fixed missing favorite count in the CivitAI Model Browser +- Fixed recommended models not showing all SDXL models ## v2.10.0-dev.2 ### Added diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index f6d7d011..df122b14 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -325,7 +325,7 @@ public static class DesignData ); }*/ - CivitAiBrowserViewModel.ModelCards = new ObservableCollection + CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended { dialogFactory.Get(vm => { diff --git a/StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs b/StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs new file mode 100644 index 00000000..59373816 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace StabilityMatrix.Avalonia.Models; + +public interface IInfinitelyScroll +{ + Task LoadNextPageAsync(); +} diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs index 924d17ed..4754397d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs @@ -1,13 +1,10 @@ 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; @@ -15,12 +12,15 @@ using Avalonia.Controls; using Avalonia.Controls.Notifications; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DynamicData; +using DynamicData.Alias; +using DynamicData.Binding; using LiteDB; using LiteDB.Async; using NLog; -using OneOf.Types; using Refit; using StabilityMatrix.Avalonia.Languages; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; @@ -41,24 +41,24 @@ namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; [View(typeof(CivitAiBrowserPage))] [Singleton] -public partial class CivitAiBrowserViewModel : TabViewModelBase +public partial class CivitAiBrowserViewModel : TabViewModelBase, IInfinitelyScroll { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly ICivitApi civitApi; - private readonly IDownloadService downloadService; private readonly ISettingsManager settingsManager; private readonly ServiceManager dialogFactory; private readonly ILiteDbContext liteDbContext; private readonly INotificationService notificationService; private const int MaxModelsPerPage = 20; + private LRUCache< int /* model id */ , CheckpointBrowserCardViewModel - > cache = new(50); + > cache = new(150); [ObservableProperty] - private ObservableCollection? modelCards; + private ObservableCollection modelCards = new(); [ObservableProperty] private DataGridCollectionView? modelCardsView; @@ -81,27 +81,9 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase [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; @@ -117,7 +99,8 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase [ObservableProperty] private bool showSantaHats = true; - private List allModelCards = new(); + [ObservableProperty] + private string? nextPageCursor; public IEnumerable AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast(); @@ -143,25 +126,11 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase ) { 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(this, nameof(PropertyChanged)) - .Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber)) - .Throttle(TimeSpan.FromMilliseconds(250)) - .Select, int>(_ => CurrentPageNumber) - .Where(page => page <= TotalPages && page > 0) - .ObserveOn(SynchronizationContext.Current) - .Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err)); - EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; } @@ -171,7 +140,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase return; SearchQuery = $"$#{e}"; - SearchModelsCommand.ExecuteAsync(null).SafeFireAndForget(); + SearchModelsCommand.ExecuteAsync(false).SafeFireAndForget(); } public override void OnLoaded() @@ -223,7 +192,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase /// /// Background update task /// - private async Task CivitModelQuery(CivitModelsRequest request) + private async Task CivitModelQuery(CivitModelsRequest request, bool isInfiniteScroll = false) { var timer = Stopwatch.StartNew(); var queryText = request.Query; @@ -276,15 +245,9 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase } ); - 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"); - } + UpdateModelCards(models, isInfiniteScroll); + + NextPageCursor = modelsResponse.Metadata?.NextCursor; } catch (OperationCanceledException) { @@ -327,7 +290,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase /// /// Updates model cards using api response object. /// - private void UpdateModelCards(IEnumerable? models, CivitMetadata? metadata) + private void UpdateModelCards(List? models, bool addCards = false) { if (models is null) { @@ -335,7 +298,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase } else { - var updateCards = models + var modelsToAdd = models .Select(model => { var cachedViewModel = cache.Get(model.Id); @@ -364,23 +327,34 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase return newCard; }) - .ToList(); + .Where(FilterModelCardsPredicate); - allModelCards = updateCards; - - var filteredCards = updateCards.Where(FilterModelCardsPredicate); if (SortMode == CivitSortMode.Installed) { - filteredCards = filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available"); + modelsToAdd = modelsToAdd.OrderByDescending(x => x.UpdateCardText == "Update Available"); + } + + if (!addCards) + { + ModelCards = new ObservableCollection(modelsToAdd); } + else + { + foreach (var model in modelsToAdd) + { + if ( + ModelCards.Contains( + model, + new PropertyComparer(x => x.CivitModel.Id) + ) + ) + continue; - ModelCards = new ObservableCollection(filteredCards); + ModelCards.Add(model); + } + } } - TotalPages = metadata?.TotalPages ?? 1; - CanGoToFirstPage = CurrentPageNumber != 1; - CanGoToPreviousPage = CurrentPageNumber > 1; - CanGoToNextPage = CurrentPageNumber < TotalPages; - CanGoToLastPage = CurrentPageNumber != TotalPages; + // Status update ShowMainLoadingSpinner = false; IsIndeterminate = false; @@ -390,27 +364,30 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase private string previousSearchQuery = string.Empty; [RelayCommand] - private async Task SearchModels() + private async Task SearchModels(bool isInfiniteScroll = false) { var timer = Stopwatch.StartNew(); - if (SearchQuery != previousSearchQuery) + if (SearchQuery != previousSearchQuery || !isInfiniteScroll) { // Reset page number - CurrentPageNumber = 1; previousSearchQuery = SearchQuery; + NextPageCursor = null; } // Build request var modelRequest = new CivitModelsRequest { - Limit = MaxModelsPerPage, Nsfw = "true", // Handled by local view filter Sort = SortMode, - Period = SelectedPeriod, - Page = CurrentPageNumber + Period = SelectedPeriod }; + if (NextPageCursor != null) + { + modelRequest.Cursor = NextPageCursor; + } + if (SelectedModelType != CivitModelType.All) { modelRequest.Types = [SelectedModelType]; @@ -516,14 +493,15 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase modelRequest.GetHashCode(), elapsed.TotalSeconds ); - UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata); + NextPageCursor = cachedQuery.Metadata?.NextCursor; + UpdateModelCards(cachedQuery.Items, isInfiniteScroll); // 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(); + CivitModelQuery(modelRequest, isInfiniteScroll).SafeFireAndForget(); Logger.Debug( "Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query", timeSinceCache.Value.TotalSeconds @@ -534,54 +512,23 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase { // Not cached, wait for remote query ShowMainLoadingSpinner = true; - await CivitModelQuery(modelRequest); + await CivitModelQuery(modelRequest, isInfiniteScroll); } 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) + public async Task LoadNextPageAsync() { - settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value); - // ModelCardsView?.Refresh(); - var updateCards = allModelCards.Where(FilterModelCardsPredicate); - ModelCards = new ObservableCollection(updateCards); - - if (!HasSearched) - return; - - UpdateResultsText(); + if (NextPageCursor != null) + { + await SearchModelsCommand.ExecuteAsync(true); + } } partial void OnSelectedPeriodChanged(CivitPeriod value) @@ -596,6 +543,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase SelectedBaseModelType ) ); + NextPageCursor = null; } partial void OnSortModeChanged(CivitSortMode value) @@ -610,6 +558,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase SelectedBaseModelType ) ); + NextPageCursor = null; } partial void OnSelectedModelTypeChanged(CivitModelType value) @@ -624,6 +573,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase SelectedBaseModelType ) ); + NextPageCursor = null; } partial void OnSelectedBaseModelTypeChanged(string value) @@ -638,6 +588,7 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase value ) ); + NextPageCursor = null; } private async Task TrySearchAgain(bool shouldUpdatePageNumber = true) @@ -648,18 +599,17 @@ public partial class CivitAiBrowserViewModel : TabViewModelBase if (shouldUpdatePageNumber) { - CurrentPageNumber = 1; + NextPageCursor = null; } // execute command instead of calling method directly so that the IsRunning property gets updated - await SearchModelsCommand.ExecuteAsync(null); + await SearchModelsCommand.ExecuteAsync(false); } private void UpdateResultsText() { NoResultsFound = ModelCards?.Count <= 0; - NoResultsText = - allModelCards.Count > 0 ? $"{allModelCards.Count} results hidden by filters" : "No results found"; + NoResultsText = "No results found"; } public override string Header => Resources.Label_CivitAi; diff --git a/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml index ba065242..25755a05 100644 --- a/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml @@ -16,6 +16,7 @@ xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" xmlns:controls1="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" + xmlns:system="clr-namespace:System;assembly=System.Runtime" d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}" d:DesignHeight="700" d:DesignWidth="800" @@ -71,6 +72,7 @@ + False @@ -336,7 +338,7 @@ + Text="{Binding CivitModel.Stats.ThumbsUpCount, Converter={StaticResource KiloFormatterConverter}}" /> - - - - - - - - - - - - - - - + + + + + + + + + ? Files { get; set; } - + [JsonPropertyName("images")] public List? Images { get; set; } - + [JsonPropertyName("stats")] public CivitModelStats Stats { get; set; } + + [JsonPropertyName("publishedAt")] + public DateTimeOffset? PublishedAt { get; set; } } diff --git a/StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs b/StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs index 66ea7c48..47464194 100644 --- a/StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs +++ b/StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs @@ -2,7 +2,6 @@ namespace StabilityMatrix.Core.Models.Api; - public class CivitModelsRequest { /// @@ -10,99 +9,99 @@ public class CivitModelsRequest /// [AliasAs("limit")] public int? Limit { get; set; } - + /// /// The page from which to start fetching models /// [AliasAs("page")] public int? Page { get; set; } - + /// /// Search query to filter models by name /// [AliasAs("query")] public string? Query { get; set; } - + /// /// Search query to filter models by tag /// [AliasAs("tag")] public string? Tag { get; set; } - + /// /// Search query to filter models by user /// [AliasAs("username")] public string? Username { get; set; } - + /// /// The type of model you want to filter with. If none is specified, it will return all types /// [AliasAs("types")] public CivitModelType[]? Types { get; set; } - + /// /// The order in which you wish to sort the results /// [AliasAs("sort")] public CivitSortMode? Sort { get; set; } - + /// /// The time frame in which the models will be sorted /// [AliasAs("period")] public CivitPeriod? Period { get; set; } - + /// /// The rating you wish to filter the models with. If none is specified, it will return models with any rating /// [AliasAs("rating")] public int? Rating { get; set; } - + /// /// Filter to models that require or don't require crediting the creator /// Requires Authentication /// [AliasAs("favorites")] public bool? Favorites { get; set; } - + /// /// Filter to hidden models of the authenticated user /// Requires Authentication /// [AliasAs("hidden")] public bool? Hidden { get; set; } - + /// /// Only include the primary file for each model (This will use your preferred format options if you use an API token or session cookie) /// [AliasAs("primaryFileOnly")] public bool? PrimaryFileOnly { get; set; } - + /// /// Filter to models that allow or don't allow creating derivatives /// [AliasAs("allowDerivatives")] public bool? AllowDerivatives { get; set; } - + /// /// Filter to models that allow or don't allow derivatives to have a different license /// [AliasAs("allowDifferentLicenses")] public bool? AllowDifferentLicenses { get; set; } - + /// /// Filter to models based on their commercial permissions /// [AliasAs("allowCommercialUse")] public CivitCommercialUse? AllowCommercialUse { get; set; } - + /// /// If false, will return safer images and hide models that don't have safe images /// [AliasAs("nsfw")] public string? Nsfw { get; set; } - + /// /// options:
/// SD 1.4
@@ -117,22 +116,25 @@ public class CivitModelsRequest ///
[AliasAs("baseModels")] public string? BaseModel { get; set; } - + [AliasAs("ids")] public string CommaSeparatedModelIds { get; set; } + [AliasAs("cursor")] + public string? Cursor { get; set; } + public override string ToString() { - return $"Page: {Page}, " + - $"Query: {Query}, " + - $"Tag: {Tag}, " + - $"Username: {Username}, " + - $"Types: {Types}, " + - $"Sort: {Sort}, " + - $"Period: {Period}, " + - $"Rating: {Rating}, " + - $"Nsfw: {Nsfw}, " + - $"BaseModel: {BaseModel}, " + - $"CommaSeparatedModelIds: {CommaSeparatedModelIds}"; + return $"Page: {Page}, " + + $"Query: {Query}, " + + $"Tag: {Tag}, " + + $"Username: {Username}, " + + $"Types: {Types}, " + + $"Sort: {Sort}, " + + $"Period: {Period}, " + + $"Rating: {Rating}, " + + $"Nsfw: {Nsfw}, " + + $"BaseModel: {BaseModel}, " + + $"CommaSeparatedModelIds: {CommaSeparatedModelIds}"; } }