Browse Source

Merge pull request #575 from ionite34/model-browser-fixes

Converted civitai browser to new pagination cursor stuff
pull/629/head
JT 8 months ago committed by GitHub
parent
commit
0eb88e2255
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 2
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  3. 8
      StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs
  4. 178
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  5. 66
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml
  6. 16
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs
  7. 4
      StabilityMatrix.Core/Models/Api/CivitMetadata.cs
  8. 3
      StabilityMatrix.Core/Models/Api/CivitModelStats.cs
  9. 3
      StabilityMatrix.Core/Models/Api/CivitModelVersion.cs
  10. 26
      StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs

6
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

2
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -325,7 +325,7 @@ public static class DesignData
);
}*/
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
CivitAiBrowserViewModel.ModelCards = new ObservableCollectionExtended<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{

8
StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Models;
public interface IInfinitelyScroll
{
Task LoadNextPageAsync();
}

178
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<ViewModelBase> 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<CheckpointBrowserCardViewModel>? modelCards;
private ObservableCollection<CheckpointBrowserCardViewModel> 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<CheckpointBrowserCardViewModel> allModelCards = new();
[ObservableProperty]
private string? nextPageCursor;
public IEnumerable<CivitPeriod> AllCivitPeriods =>
Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>();
@ -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<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select<EventPattern<PropertyChangedEventArgs>, 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
/// <summary>
/// Background update task
/// </summary>
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
/// <summary>
/// Updates model cards using api response object.
/// </summary>
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata)
private void UpdateModelCards(List<CivitModel>? 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();
allModelCards = updateCards;
.Where(FilterModelCardsPredicate);
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");
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
if (!addCards)
{
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(modelsToAdd);
}
else
{
foreach (var model in modelsToAdd)
{
if (
ModelCards.Contains(
model,
new PropertyComparer<CheckpointBrowserCardViewModel>(x => x.CivitModel.Id)
)
)
continue;
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<CheckpointBrowserCardViewModel>(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;

66
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 @@
</UserControl.Styles>
<UserControl.Resources>
<system:Boolean x:Key="False">False</system:Boolean>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border
@ -107,7 +109,7 @@
HorizontalAlignment="Center"
Height="75"
ZIndex="10"
IsVisible="{Binding ShowSantaHats}"
IsVisible="{Binding ShowSantaHats, FallbackValue=False}"
Margin="0,8,0,0"
Source="avares://StabilityMatrix.Avalonia/Assets/santahat.png">
<!-- <controls:BetterAdvancedImage.RenderTransform> -->
@ -336,7 +338,7 @@
<TextBlock
Margin="4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.ModelVersionStats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" />
Text="{Binding CivitModel.Stats.ThumbsUpCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
@ -402,6 +404,7 @@
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchModelsCommand}"
CommandParameter="{StaticResource False}"
IsDefault="True">
<Grid>
<controls:ProgressRing
@ -473,56 +476,17 @@
</ItemsRepeater>
</ScrollViewer>
<TextBlock
Grid.Row="2"
Margin="8,8"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" />
<StackPanel Grid.Row="2"
HorizontalAlignment="Center"
IsVisible="{Binding HasSearched}"
Margin="0,8,0,8"
Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,16,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<TextBlock Margin="8,0,4,0" TextAlignment="Center"
Text="{x:Static lang:Resources.Label_Page}"
VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding CurrentPageNumber, FallbackValue=1}"
VerticalAlignment="Center"
SpinButtonPlacementMode="Hidden"
TextAlignment="Center"/>
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center">
<Run Text="/"/>
<Run Text="{Binding TotalPages, FallbackValue=5}"/>
<TextBlock Grid.Row="2" Text="End of results"
TextAlignment="Center"
Margin="0,0,0,8">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="HasSearched"/>
<Binding Path="NextPageCursor"
Converter="{x:Static StringConverters.IsNullOrEmpty}"/>
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<Button
Margin="16,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
<TextBlock
Grid.Row="0"

16
StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs

@ -1,8 +1,11 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Attributes;
using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel;
@ -26,8 +29,15 @@ public partial class CivitAiBrowserPage : UserControlBase
if (sender is not ScrollViewer scrollViewer)
return;
var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum;
Debug.WriteLine($"IsAtEnd: {isAtEnd}");
if (scrollViewer.Offset.Y == 0)
return;
var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f;
if (isAtEnd && DataContext is IInfinitelyScroll scroll)
{
scroll.LoadNextPageAsync().SafeFireAndForget();
}
}
private void InputElement_OnKeyDown(object? sender, KeyEventArgs e)

4
StabilityMatrix.Core/Models/Api/CivitMetadata.cs

@ -2,7 +2,6 @@
namespace StabilityMatrix.Core.Models.Api;
public class CivitMetadata
{
[JsonPropertyName("totalItems")]
@ -22,4 +21,7 @@ public class CivitMetadata
[JsonPropertyName("prevPage")]
public string? PrevPage { get; set; }
[JsonPropertyName("nextCursor")]
public string? NextCursor { get; set; }
}

3
StabilityMatrix.Core/Models/Api/CivitModelStats.cs

@ -9,4 +9,7 @@ public class CivitModelStats : CivitStats
[JsonPropertyName("commentCount")]
public int CommentCount { get; set; }
[JsonPropertyName("thumbsUpCount")]
public int ThumbsUpCount { get; set; }
}

3
StabilityMatrix.Core/Models/Api/CivitModelVersion.cs

@ -33,4 +33,7 @@ public class CivitModelVersion
[JsonPropertyName("stats")]
public CivitModelStats Stats { get; set; }
[JsonPropertyName("publishedAt")]
public DateTimeOffset? PublishedAt { get; set; }
}

26
StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs

@ -2,7 +2,6 @@
namespace StabilityMatrix.Core.Models.Api;
public class CivitModelsRequest
{
/// <summary>
@ -121,18 +120,21 @@ public class CivitModelsRequest
[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}";
}
}

Loading…
Cancel
Save