JT
1 year ago
18 changed files with 1026 additions and 33 deletions
After Width: | Height: | Size: 15 KiB |
@ -1,23 +1,14 @@
|
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Primitives; |
||||
using Avalonia.Styling; |
||||
using System; |
||||
using Avalonia.Controls; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
public class Card : Expander |
||||
{ |
||||
protected override Type StyleKeyOverride => typeof(Expander); |
||||
|
||||
public Card() |
||||
{ |
||||
// Expander /template/ ToggleButton#PART_toggle |
||||
var customStyle = new Style(x => |
||||
x.OfType<Expander>().Template().OfType<ToggleButton>().Name("PART_toggle")); |
||||
|
||||
customStyle.Setters.Add(new Setter |
||||
{ |
||||
Property = IsVisibleProperty, |
||||
Value = false |
||||
}); |
||||
|
||||
Styles.Add(customStyle); |
||||
IsExpanded = true; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,22 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"> |
||||
<Design.PreviewWith> |
||||
<Border Padding="20"> |
||||
<controls:Card |
||||
Margin="8" |
||||
MaxHeight="450" |
||||
IsExpanded="True" |
||||
Header="Header Text" |
||||
Name="ModelCard" |
||||
Width="330"> |
||||
<TextBlock Text="Hello World"/> |
||||
</controls:Card> |
||||
</Border> |
||||
</Design.PreviewWith> |
||||
|
||||
<Style Selector="Expander /template/ ToggleButton#PART_toggle"> |
||||
<Setter Property="IsVisible" Value="False"></Setter> |
||||
</Style> |
||||
|
||||
</Styles> |
@ -0,0 +1,269 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Net.Http; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Media.Imaging; |
||||
using Avalonia.Threading; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using FluentAvalonia.UI.Controls; |
||||
using NLog; |
||||
using StabilityMatrix.Core.Extensions; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Api; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
using StabilityMatrix.Core.Processes; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
public partial class CheckpointBrowserCardViewModel : ProgressViewModel |
||||
|
||||
{ |
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||
private readonly IDownloadService downloadService; |
||||
private readonly ISettingsManager settingsManager; |
||||
public CivitModel CivitModel { get; init; } |
||||
public Bitmap? CardImage { get; set; } |
||||
public override bool IsTextVisible => Value > 0; |
||||
|
||||
[ObservableProperty] private bool isImporting; |
||||
|
||||
public CheckpointBrowserCardViewModel( |
||||
CivitModel civitModel, |
||||
IDownloadService downloadService, |
||||
ISettingsManager settingsManager, |
||||
Bitmap? fixedImage = null) |
||||
{ |
||||
this.downloadService = downloadService; |
||||
this.settingsManager = settingsManager; |
||||
CivitModel = civitModel; |
||||
|
||||
if (fixedImage != null) |
||||
{ |
||||
CardImage = fixedImage; |
||||
return; |
||||
} |
||||
|
||||
if (Design.IsDesignMode) return; |
||||
|
||||
UpdateImage().SafeFireAndForget(); |
||||
|
||||
// Update image when nsfw setting changes |
||||
settingsManager.RegisterPropertyChangedHandler( |
||||
s => s.ModelBrowserNsfwEnabled, |
||||
_ => UpdateImage().SafeFireAndForget()); |
||||
} |
||||
|
||||
// Choose and load image based on nsfw setting |
||||
private async Task UpdateImage() |
||||
{ |
||||
var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled; |
||||
var version = CivitModel.ModelVersions?.FirstOrDefault(); |
||||
var images = version?.Images; |
||||
|
||||
var image = images?.FirstOrDefault(image => nsfwEnabled || image.Nsfw == "None"); |
||||
if (image != null) |
||||
{ |
||||
var imageStream = await downloadService.GetImageStreamFromUrl(image.Url); |
||||
Dispatcher.UIThread.Invoke(() => |
||||
{ |
||||
CardImage = new Bitmap(imageStream); |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
// Otherwise Default image |
||||
Dispatcher.UIThread.Invoke(() => |
||||
{ |
||||
CardImage = new Bitmap("Assets/noimage.png"); |
||||
}); |
||||
} |
||||
|
||||
// On any mode changes, update the image |
||||
private void OnNsfwModeChanged(object? sender, bool value) |
||||
{ |
||||
UpdateImage().SafeFireAndForget(); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private void OpenModel() |
||||
{ |
||||
ProcessRunner.OpenUrl($"https://civitai.com/models/{CivitModel.Id}"); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task Import(CivitModel model) |
||||
{ |
||||
await DoImport(model); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task ShowVersionDialog(CivitModel model) |
||||
{ |
||||
// var dialog = dialogFactory.CreateSelectModelVersionDialog(model); |
||||
// var result = await dialog.ShowAsync(); |
||||
// |
||||
// if (result != ContentDialogResult.Primary) |
||||
// { |
||||
// return; |
||||
// } |
||||
// |
||||
// var viewModel = dialog.DataContext as SelectModelVersionDialogViewModel; |
||||
// var selectedVersion = viewModel?.SelectedVersion; |
||||
// var selectedFile = viewModel?.SelectedFile; |
||||
// |
||||
// await Task.Delay(100); |
||||
// await DoImport(model, selectedVersion, selectedFile); |
||||
} |
||||
|
||||
private async Task DoImport(CivitModel model, CivitModelVersion? selectedVersion = null, CivitFile? selectedFile = null) |
||||
{ |
||||
IsImporting = true; |
||||
Text = "Downloading..."; |
||||
|
||||
// Holds files to be deleted on errors |
||||
var filesForCleanup = new HashSet<FilePath>(); |
||||
|
||||
// Set Text when exiting, finally block will set 100 and delay clear progress |
||||
try |
||||
{ |
||||
// Get latest version |
||||
var modelVersion = selectedVersion ?? model.ModelVersions?.FirstOrDefault(); |
||||
if (modelVersion is null) |
||||
{ |
||||
// snackbarService.ShowSnackbarAsync( |
||||
// "This model has no versions available for download", |
||||
// "Model has no versions available", ControlAppearance.Caution).SafeFireAndForget(); |
||||
Text = "Unable to Download"; |
||||
return; |
||||
} |
||||
|
||||
// Get latest version file |
||||
var modelFile = selectedFile ?? modelVersion.Files?.FirstOrDefault(); |
||||
if (modelFile is null) |
||||
{ |
||||
// snackbarService.ShowSnackbarAsync( |
||||
// "This model has no files available for download", |
||||
// "Model has no files available", ControlAppearance.Caution).SafeFireAndForget(); |
||||
Text = "Unable to Download"; |
||||
return; |
||||
} |
||||
|
||||
var downloadFolder = Path.Combine(settingsManager.ModelsDirectory, |
||||
model.Type.ConvertTo<SharedFolderType>().GetStringValue()); |
||||
// Folders might be missing if user didn't install any packages yet |
||||
Directory.CreateDirectory(downloadFolder); |
||||
var downloadPath = Path.GetFullPath(Path.Combine(downloadFolder, modelFile.Name)); |
||||
filesForCleanup.Add(downloadPath); |
||||
|
||||
// Do the download |
||||
var downloadTask = downloadService.DownloadToFileAsync(modelFile.DownloadUrl, downloadPath, |
||||
new Progress<ProgressReport>(report => |
||||
{ |
||||
Dispatcher.UIThread.Invoke(() => |
||||
{ |
||||
Value = report.Percentage; |
||||
Text = $"Downloading... {report.Percentage}%"; |
||||
}); |
||||
})); |
||||
|
||||
// var downloadResult = await snackbarService.TryAsync(downloadTask, "Could not download file"); |
||||
|
||||
// Failed download handling |
||||
// if (downloadResult.Exception is not null) |
||||
// { |
||||
// // For exceptions other than ApiException or TaskCanceledException, log error |
||||
// var logLevel = downloadResult.Exception switch |
||||
// { |
||||
// HttpRequestException or ApiException or TaskCanceledException => LogLevel.Warn, |
||||
// _ => LogLevel.Error |
||||
// }; |
||||
// Logger.Log(logLevel, downloadResult.Exception, "Error during model download"); |
||||
// |
||||
// Text = "Download Failed"; |
||||
// return; |
||||
//} |
||||
|
||||
// When sha256 is available, validate the downloaded file |
||||
var fileExpectedSha256 = modelFile.Hashes.SHA256; |
||||
if (!string.IsNullOrEmpty(fileExpectedSha256)) |
||||
{ |
||||
var hashProgress = new Progress<ProgressReport>(progress => |
||||
{ |
||||
Value = progress.Percentage; |
||||
Text = $"Validating... {progress.Percentage}%"; |
||||
}); |
||||
var sha256 = await FileHash.GetSha256Async(downloadPath, hashProgress); |
||||
if (sha256 != fileExpectedSha256.ToLowerInvariant()) |
||||
{ |
||||
Text = "Import Failed!"; |
||||
DelayedClearProgress(TimeSpan.FromMilliseconds(800)); |
||||
// snackbarService.ShowSnackbarAsync( |
||||
// "This may be caused by network or server issues from CivitAI, please try again in a few minutes.", |
||||
// "Download failed hash validation").SafeFireAndForget(); |
||||
Text = "Download Failed"; |
||||
return; |
||||
} |
||||
// snackbarService.ShowSnackbarAsync($"{model.Type} {model.Name} imported successfully!", |
||||
// "Import complete", ControlAppearance.Info).SafeFireAndForget(); |
||||
} |
||||
|
||||
IsIndeterminate = true; |
||||
|
||||
// Save connected model info |
||||
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name); |
||||
var modelInfo = new ConnectedModelInfo(CivitModel, modelVersion, modelFile, DateTime.UtcNow); |
||||
var modelInfoPath = Path.GetFullPath(Path.Combine( |
||||
downloadFolder, modelFileName + ConnectedModelInfo.FileExtension)); |
||||
filesForCleanup.Add(modelInfoPath); |
||||
await modelInfo.SaveJsonToDirectory(downloadFolder, modelFileName); |
||||
|
||||
// If available, save a model image |
||||
if (modelVersion.Images != null && modelVersion.Images.Any()) |
||||
{ |
||||
var image = modelVersion.Images[0]; |
||||
var imageExtension = Path.GetExtension(image.Url).TrimStart('.'); |
||||
if (imageExtension is "jpg" or "jpeg" or "png") |
||||
{ |
||||
var imageDownloadPath = Path.GetFullPath(Path.Combine(downloadFolder, $"{modelFileName}.preview.{imageExtension}")); |
||||
filesForCleanup.Add(imageDownloadPath); |
||||
var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); |
||||
// await snackbarService.TryAsync(imageTask, "Could not download preview image"); |
||||
} |
||||
} |
||||
|
||||
// Successful - clear cleanup list |
||||
filesForCleanup.Clear(); |
||||
|
||||
Text = "Import complete!"; |
||||
} |
||||
finally |
||||
{ |
||||
foreach (var file in filesForCleanup.Where(file => file.Exists)) |
||||
{ |
||||
file.Delete(); |
||||
Logger.Info($"Download cleanup: Deleted file {file}"); |
||||
} |
||||
IsIndeterminate = false; |
||||
Value = 100; |
||||
DelayedClearProgress(TimeSpan.FromMilliseconds(800)); |
||||
} |
||||
} |
||||
|
||||
private void DelayedClearProgress(TimeSpan delay) |
||||
{ |
||||
Task.Delay(delay).ContinueWith(_ => |
||||
{ |
||||
Text = string.Empty; |
||||
Value = 0; |
||||
IsImporting = false; |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,381 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.ObjectModel; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using System.Net.Http; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Collections; |
||||
using Avalonia.Controls; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using FluentAvalonia.UI.Controls; |
||||
using FluentAvalonia.UI.Data; |
||||
using NLog; |
||||
using StabilityMatrix.Avalonia.Views; |
||||
using StabilityMatrix.Core.Api; |
||||
using StabilityMatrix.Core.Attributes; |
||||
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.Settings; |
||||
using StabilityMatrix.Core.Services; |
||||
using ApiException = Refit.ApiException; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
[View(typeof(CheckpointBrowserPage))] |
||||
public partial class CheckpointBrowserViewModel : PageViewModelBase |
||||
{ |
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||
private readonly ICivitApi civitApi; |
||||
private readonly IDownloadService downloadService; |
||||
private readonly ISettingsManager settingsManager; |
||||
private readonly ILiteDbContext liteDbContext; |
||||
private const int MaxModelsPerPage = 14; |
||||
|
||||
[ObservableProperty] private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards; |
||||
[ObservableProperty] private DataGridCollectionView? modelCardsView; |
||||
|
||||
[ObservableProperty] private string searchQuery = string.Empty; |
||||
[ObservableProperty] private bool showNsfw; |
||||
[ObservableProperty] private bool showMainLoadingSpinner; |
||||
[ObservableProperty] private CivitPeriod selectedPeriod = CivitPeriod.Month; |
||||
[ObservableProperty] private CivitSortMode sortMode = CivitSortMode.HighestRated; |
||||
[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 isIndeterminate; |
||||
[ObservableProperty] private bool noResultsFound; |
||||
[ObservableProperty] private string noResultsText; |
||||
|
||||
public IEnumerable<CivitPeriod> AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>(); |
||||
public IEnumerable<CivitSortMode> AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast<CivitSortMode>(); |
||||
|
||||
public IEnumerable<CivitModelType> AllModelTypes => Enum.GetValues(typeof(CivitModelType)) |
||||
.Cast<CivitModelType>() |
||||
.Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0) |
||||
.OrderBy(t => t.ToString()); |
||||
|
||||
public CheckpointBrowserViewModel( |
||||
ICivitApi civitApi, |
||||
IDownloadService downloadService, |
||||
ISettingsManager settingsManager, |
||||
ILiteDbContext liteDbContext) |
||||
{ |
||||
this.civitApi = civitApi; |
||||
this.downloadService = downloadService; |
||||
this.settingsManager = settingsManager; |
||||
this.liteDbContext = liteDbContext; |
||||
|
||||
CurrentPageNumber = 1; |
||||
CanGoToNextPage = true; |
||||
} |
||||
|
||||
public override void OnLoaded() |
||||
{ |
||||
if (Design.IsDesignMode) return; |
||||
|
||||
var searchOptions = settingsManager.Settings.ModelSearchOptions; |
||||
|
||||
SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month; |
||||
SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated; |
||||
SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint; |
||||
|
||||
ShowNsfw = settingsManager.Settings.ModelBrowserNsfwEnabled; |
||||
|
||||
settingsManager.RelayPropertyFor(this, model => model.ShowNsfw, |
||||
settings => settings.ModelBrowserNsfwEnabled); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Filter predicate for model cards |
||||
/// </summary> |
||||
private bool FilterModelCardsPredicate(object? item) |
||||
{ |
||||
if (item is not CheckpointBrowserCardViewModel card) return false; |
||||
return !card.CivitModel.Nsfw || ShowNsfw; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Background update task |
||||
/// </summary> |
||||
private async Task CivitModelQuery(CivitModelsRequest request) |
||||
{ |
||||
var timer = Stopwatch.StartNew(); |
||||
var queryText = request.Query; |
||||
try |
||||
{ |
||||
var modelsResponse = await civitApi.GetModels(request); |
||||
var models = modelsResponse.Items; |
||||
if (models is null) |
||||
{ |
||||
Logger.Debug("CivitAI Query {Text} returned no results (in {Elapsed:F1} s)", |
||||
queryText, timer.Elapsed.TotalSeconds); |
||||
return; |
||||
} |
||||
|
||||
Logger.Debug("CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)", |
||||
queryText, models.Count, timer.Elapsed.TotalSeconds); |
||||
|
||||
var unknown = models.Where(m => m.Type == CivitModelType.Unknown).ToList(); |
||||
if (unknown.Any()) |
||||
{ |
||||
var names = unknown.Select(m => m.Name).ToList(); |
||||
Logger.Warn("Excluded {Unknown} unknown model types: {Models}", unknown.Count, |
||||
names); |
||||
} |
||||
|
||||
// Filter out unknown model types and archived/taken-down models |
||||
models = models.Where(m => m.Type.ConvertTo<SharedFolderType>() > 0) |
||||
.Where(m => m.Mode == null).ToList(); |
||||
|
||||
// Database update calls will invoke `OnModelsUpdated` |
||||
// Add to database |
||||
await liteDbContext.UpsertCivitModelAsync(models); |
||||
// Add as cache entry |
||||
var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync(new() |
||||
{ |
||||
Id = ObjectHash.GetMd5Guid(request), |
||||
InsertedAt = DateTimeOffset.UtcNow, |
||||
Request = request, |
||||
Items = models, |
||||
Metadata = modelsResponse.Metadata |
||||
}); |
||||
|
||||
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"); |
||||
} |
||||
} |
||||
catch (OperationCanceledException) |
||||
{ |
||||
// snackbarService.ShowSnackbarAsync("Request to CivitAI timed out", |
||||
// "Please try again in a few minutes").SafeFireAndForget(); |
||||
Logger.Warn($"CivitAI query timed out ({request})"); |
||||
} |
||||
catch (HttpRequestException e) |
||||
{ |
||||
// snackbarService.ShowSnackbarAsync("CivitAI can't be reached right now", |
||||
// "Please try again in a few minutes").SafeFireAndForget(); |
||||
Logger.Warn(e, $"CivitAI query HttpRequestException ({request})"); |
||||
} |
||||
catch (ApiException e) |
||||
{ |
||||
// snackbarService.ShowSnackbarAsync("CivitAI can't be reached right now", |
||||
// "Please try again in a few minutes").SafeFireAndForget(); |
||||
Logger.Warn(e, $"CivitAI query ApiException ({request})"); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
// snackbarService.ShowSnackbarAsync($"Please try again in a few minutes", |
||||
// $"Unknown exception during CivitAI query: {e.GetType().Name}").SafeFireAndForget(); |
||||
Logger.Error(e, $"CivitAI query unknown exception ({request})"); |
||||
} |
||||
finally |
||||
{ |
||||
ShowMainLoadingSpinner = false; |
||||
UpdateResultsText(); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Updates model cards using api response object. |
||||
/// </summary> |
||||
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata) |
||||
{ |
||||
if (models is null) |
||||
{ |
||||
ModelCards?.Clear(); |
||||
} |
||||
else |
||||
{ |
||||
var updateCards = models |
||||
.Select(model => new CheckpointBrowserCardViewModel(model, |
||||
downloadService, settingsManager)); |
||||
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards); |
||||
} |
||||
TotalPages = metadata?.TotalPages ?? 1; |
||||
CanGoToPreviousPage = CurrentPageNumber > 1; |
||||
CanGoToNextPage = CurrentPageNumber < TotalPages; |
||||
// Status update |
||||
ShowMainLoadingSpinner = false; |
||||
IsIndeterminate = false; |
||||
HasSearched = true; |
||||
} |
||||
|
||||
private string previousSearchQuery = string.Empty; |
||||
|
||||
[RelayCommand] |
||||
private async Task SearchModels() |
||||
{ |
||||
var timer = Stopwatch.StartNew(); |
||||
|
||||
if (SearchQuery != previousSearchQuery) |
||||
{ |
||||
// Reset page number |
||||
CurrentPageNumber = 1; |
||||
previousSearchQuery = SearchQuery; |
||||
} |
||||
|
||||
// Build request |
||||
var modelRequest = new CivitModelsRequest |
||||
{ |
||||
Limit = MaxModelsPerPage, |
||||
Nsfw = "true", // Handled by local view filter |
||||
Sort = SortMode, |
||||
Period = SelectedPeriod, |
||||
Page = CurrentPageNumber |
||||
}; |
||||
|
||||
if (SearchQuery.StartsWith("#")) |
||||
{ |
||||
modelRequest.Tag = SearchQuery[1..]; |
||||
} |
||||
else if (SearchQuery.StartsWith("@")) |
||||
{ |
||||
modelRequest.Username = SearchQuery[1..]; |
||||
} |
||||
else |
||||
{ |
||||
modelRequest.Query = SearchQuery; |
||||
} |
||||
|
||||
if (SelectedModelType != CivitModelType.All) |
||||
{ |
||||
modelRequest.Types = new[] {SelectedModelType}; |
||||
} |
||||
|
||||
// See if query is cached |
||||
var cachedQuery = await liteDbContext.CivitModelQueryCache |
||||
.IncludeAll() |
||||
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest)); |
||||
|
||||
// If cached, update model cards |
||||
if (cachedQuery is not null) |
||||
{ |
||||
var elapsed = timer.Elapsed; |
||||
Logger.Debug("Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)", |
||||
SearchQuery, modelRequest.GetHashCode(), elapsed.TotalSeconds); |
||||
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata); |
||||
|
||||
// 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(); |
||||
Logger.Debug( |
||||
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query", |
||||
timeSinceCache.Value.TotalSeconds); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
// Not cached, wait for remote query |
||||
ShowMainLoadingSpinner = true; |
||||
await CivitModelQuery(modelRequest); |
||||
} |
||||
|
||||
UpdateResultsText(); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task PreviousPage() |
||||
{ |
||||
if (CurrentPageNumber == 1) return; |
||||
|
||||
CurrentPageNumber--; |
||||
await TrySearchAgain(false); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task NextPage() |
||||
{ |
||||
CurrentPageNumber++; |
||||
await TrySearchAgain(false); |
||||
} |
||||
|
||||
// On changes to ModelCards, update the view source |
||||
partial void OnModelCardsChanged(ObservableCollection<CheckpointBrowserCardViewModel>? value) |
||||
{ |
||||
if (value is null) |
||||
{ |
||||
ModelCardsView = null; |
||||
} |
||||
// Create new view |
||||
var view = new DataGridCollectionView(value) |
||||
{ |
||||
Filter = FilterModelCardsPredicate, |
||||
}; |
||||
ModelCardsView = view; |
||||
} |
||||
|
||||
partial void OnShowNsfwChanged(bool value) |
||||
{ |
||||
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled = value); |
||||
ModelCardsView?.Refresh(); |
||||
|
||||
if (!HasSearched) |
||||
return; |
||||
|
||||
UpdateResultsText(); |
||||
} |
||||
|
||||
partial void OnSelectedPeriodChanged(CivitPeriod oldValue, CivitPeriod newValue) |
||||
{ |
||||
TrySearchAgain().SafeFireAndForget(); |
||||
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( |
||||
newValue, SortMode, SelectedModelType)); |
||||
} |
||||
|
||||
partial void OnSortModeChanged(CivitSortMode oldValue, CivitSortMode newValue) |
||||
{ |
||||
TrySearchAgain().SafeFireAndForget(); |
||||
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( |
||||
SelectedPeriod, newValue, SelectedModelType)); |
||||
} |
||||
|
||||
partial void OnSelectedModelTypeChanged(CivitModelType oldValue, CivitModelType newValue) |
||||
{ |
||||
TrySearchAgain().SafeFireAndForget(); |
||||
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( |
||||
SelectedPeriod, SortMode, newValue)); |
||||
} |
||||
|
||||
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true) |
||||
{ |
||||
if (!HasSearched) return; |
||||
ModelCards?.Clear(); |
||||
|
||||
if (shouldUpdatePageNumber) |
||||
{ |
||||
CurrentPageNumber = 1; |
||||
} |
||||
|
||||
// execute command instead of calling method directly so that the IsRunning property gets updated |
||||
await SearchModelsCommand.ExecuteAsync(null); |
||||
} |
||||
|
||||
private void UpdateResultsText() |
||||
{ |
||||
NoResultsFound = (ModelCards?.Count ?? 0) <= 0; |
||||
NoResultsText = ModelCards?.Count > 0 |
||||
? $"{ModelCards.Count} results hidden by filters" |
||||
: "No results found"; |
||||
} |
||||
|
||||
public override string Title => "Model Browser"; |
||||
public override Symbol Icon => Symbol.Find; |
||||
} |
@ -0,0 +1,260 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:controls1="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
x:DataType="viewModels:CheckpointBrowserViewModel" |
||||
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}" |
||||
x:CompileBindings="True" |
||||
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage"> |
||||
|
||||
<UserControl.Resources> |
||||
<DataTemplate DataType="{x:Type viewModels:CheckpointBrowserCardViewModel}" x:Key="CivitModelTemplate"> |
||||
<controls1:Card |
||||
Margin="8" |
||||
MaxHeight="450" |
||||
IsExpanded="True" |
||||
Name="ModelCard" |
||||
Width="330"> |
||||
|
||||
<StackPanel Orientation="Vertical"> |
||||
<TextBlock |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
Margin="0,0,0,0" |
||||
Text="{Binding CivitModel.Name}" |
||||
VerticalAlignment="Center" /> |
||||
<TextBlock |
||||
FontSize="11" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
Margin="0,2,0,0" |
||||
Text="{Binding CivitModel.ModelVersions[0].Name, FallbackValue=''}" |
||||
VerticalAlignment="Center" /> |
||||
<Grid> |
||||
<Image |
||||
Margin="0,8,0,8" |
||||
MaxHeight="300" |
||||
Source="{Binding CardImage}" |
||||
Stretch="UniformToFill" /> |
||||
|
||||
<!-- Appearance="Info" --> |
||||
<Button |
||||
Command="{Binding OpenModelCommand}" |
||||
CommandParameter="{Binding CivitModel}" |
||||
HorizontalAlignment="Right" |
||||
Margin="0,16,8,0" |
||||
VerticalAlignment="Top"> |
||||
<Button.Content> |
||||
<controls:SymbolIcon Symbol="Open" /> |
||||
</Button.Content> |
||||
</Button> |
||||
|
||||
<controls1:Card |
||||
Background="#AA1467B5" |
||||
Height="20" |
||||
HorizontalAlignment="Left" |
||||
Margin="4,16,0,0" |
||||
Padding="3" |
||||
VerticalAlignment="Top"> |
||||
|
||||
<TextBlock |
||||
FontSize="10" |
||||
FontWeight="Medium" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
HorizontalAlignment="Center" |
||||
Text="{Binding CivitModel.Type}" |
||||
VerticalAlignment="Center" /> |
||||
</controls1:Card> |
||||
|
||||
<Rectangle |
||||
Fill="#DD000000" |
||||
HorizontalAlignment="Stretch" |
||||
Margin="0,8,0,8" |
||||
VerticalAlignment="Stretch" |
||||
IsVisible="{Binding IsImporting, FallbackValue=True}" /> |
||||
<StackPanel |
||||
HorizontalAlignment="Stretch" |
||||
Orientation="Vertical" |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding IsImporting}"> |
||||
<controls1:ProgressRing |
||||
HorizontalAlignment="Center" |
||||
IsIndeterminate="False" |
||||
Value="{Binding Value}" |
||||
VerticalAlignment="Center" /> |
||||
<TextBlock |
||||
HorizontalAlignment="Center" |
||||
Margin="0,8,0,0" |
||||
Text="{Binding Text, FallbackValue=Importing...}" |
||||
VerticalAlignment="Center" /> |
||||
</StackPanel> |
||||
</Grid> |
||||
<Grid> |
||||
<Grid.ColumnDefinitions> |
||||
<ColumnDefinition Width="1*" /> |
||||
<ColumnDefinition Width="Auto"/> |
||||
</Grid.ColumnDefinitions> |
||||
<Button |
||||
Classes="accent" |
||||
Command="{Binding ImportCommand}" |
||||
CommandParameter="{Binding CivitModel}" |
||||
IsEnabled="{Binding !IsImporting}" |
||||
HorizontalAlignment="Stretch" |
||||
Margin="0,8,0,0"> |
||||
<Button.Content> |
||||
<StackPanel Orientation="Horizontal"> |
||||
<TextBlock Text="Import Latest -"/> |
||||
<TextBlock Margin="4,0,0,0" |
||||
Text="{Binding CivitModel.FullFilesSize}"/> |
||||
</StackPanel> |
||||
</Button.Content> |
||||
</Button> |
||||
|
||||
<Button Grid.Column="1" |
||||
Margin="8,8,0,0" |
||||
Classes="accent" |
||||
IsEnabled="{Binding !IsImporting}" |
||||
Command="{Binding ShowVersionDialogCommand}" |
||||
CommandParameter="{Binding CivitModel}" |
||||
Content="All Versions"/> |
||||
</Grid> |
||||
</StackPanel> |
||||
</controls1:Card> |
||||
</DataTemplate> |
||||
</UserControl.Resources> |
||||
|
||||
<Grid> |
||||
<Grid.RowDefinitions> |
||||
<RowDefinition Height="Auto" /> |
||||
<RowDefinition Height="*" /> |
||||
<RowDefinition Height="Auto" /> |
||||
</Grid.RowDefinitions> |
||||
|
||||
<StackPanel Margin="8" Orientation="Vertical"> |
||||
<Grid> |
||||
<Grid.ColumnDefinitions> |
||||
<ColumnDefinition Width="*" /> |
||||
<ColumnDefinition Width="Auto" /> |
||||
</Grid.ColumnDefinitions> |
||||
|
||||
<TextBox |
||||
HorizontalAlignment="Stretch" |
||||
Margin="8,0,0,0" |
||||
Watermark="Search models, #tags, or @users" |
||||
Text="{Binding SearchQuery, Mode=TwoWay}" /> |
||||
|
||||
<Button |
||||
Classes="accent" |
||||
Command="{Binding SearchModelsCommand}" |
||||
Grid.Column="1" |
||||
IsDefault="True" |
||||
Margin="8,0,8,0" |
||||
VerticalAlignment="Stretch" |
||||
Width="80"> |
||||
<StackPanel Orientation="Horizontal"> |
||||
<controls1:ProgressRing |
||||
Height="20" |
||||
IsIndeterminate="True" |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding SearchModelsCommand.IsRunning}" |
||||
Width="20" /> |
||||
<TextBlock |
||||
Text="Search" |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding !SearchModelsCommand.IsRunning}" /> |
||||
</StackPanel> |
||||
</Button> |
||||
</Grid> |
||||
<DockPanel> |
||||
<StackPanel Margin="8" Orientation="Vertical"> |
||||
<Label Content="Sort" /> |
||||
<ComboBox |
||||
ItemsSource="{Binding AllSortModes}" |
||||
MinWidth="100" |
||||
SelectedItem="{Binding SortMode}" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel Margin="0,8" Orientation="Vertical"> |
||||
<Label Content="Period" /> |
||||
<ComboBox |
||||
ItemsSource="{Binding AllCivitPeriods}" |
||||
MinWidth="100" |
||||
SelectedItem="{Binding SelectedPeriod}" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel Margin="8" Orientation="Vertical"> |
||||
<Label Content="Model Type" /> |
||||
<ComboBox |
||||
ItemsSource="{Binding AllModelTypes}" |
||||
MinWidth="100" |
||||
SelectedItem="{Binding SelectedModelType}" /> |
||||
</StackPanel> |
||||
|
||||
<CheckBox |
||||
Content="Show NSFW Content" |
||||
FontSize="12" |
||||
HorizontalAlignment="Right" |
||||
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" |
||||
Margin="8,8,8,0" /> |
||||
</DockPanel> |
||||
|
||||
</StackPanel> |
||||
<ScrollViewer Grid.Row="1"> |
||||
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}" |
||||
ItemsSource="{Binding ModelCards}"> |
||||
<ItemsRepeater.Layout> |
||||
<UniformGridLayout Orientation="Horizontal" /> |
||||
</ItemsRepeater.Layout> |
||||
</ItemsRepeater> |
||||
</ScrollViewer> |
||||
|
||||
<TextBlock |
||||
Grid.Row="2" |
||||
Margin="16,8" |
||||
Text="Data provided by CivitAI" |
||||
VerticalAlignment="Bottom" /> |
||||
|
||||
<StackPanel |
||||
Grid.Row="2" |
||||
HorizontalAlignment="Center" |
||||
Margin="8" |
||||
Orientation="Vertical" |
||||
IsVisible="{Binding HasSearched}"> |
||||
<TextBlock Margin="0,0,4,4" TextAlignment="Center"> |
||||
<Run Text="Page" /> |
||||
<Run Text="{Binding CurrentPageNumber, FallbackValue=1}" /> |
||||
<Run Text="/" /> |
||||
<Run Text="{Binding TotalPages, FallbackValue=5}" /> |
||||
</TextBlock> |
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> |
||||
<Button |
||||
Command="{Binding PreviousPageCommand}" |
||||
IsEnabled="{Binding CanGoToPreviousPage}" |
||||
Margin="0,0,8,0"> |
||||
<controls:SymbolIcon Symbol="PreviousFilled" /> |
||||
</Button> |
||||
<Button Command="{Binding NextPageCommand}" IsEnabled="{Binding CanGoToNextPage}"> |
||||
<controls:SymbolIcon Symbol="NextFilled" /> |
||||
</Button> |
||||
</StackPanel> |
||||
</StackPanel> |
||||
|
||||
<TextBlock |
||||
Grid.Row="0" |
||||
Grid.RowSpan="3" |
||||
FontSize="20" |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding NoResultsText, FallbackValue=No results found}" |
||||
IsVisible="{Binding NoResultsFound}" /> |
||||
|
||||
<controls1:ProgressRing |
||||
Grid.Row="0" |
||||
Grid.RowSpan="3" |
||||
IsIndeterminate="True" |
||||
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" /> |
||||
</Grid> |
||||
</UserControl> |
@ -0,0 +1,18 @@
|
||||
using Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Markup.Xaml; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views; |
||||
|
||||
public partial class CheckpointBrowserPage : UserControl |
||||
{ |
||||
public CheckpointBrowserPage() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
|
||||
private void InitializeComponent() |
||||
{ |
||||
AvaloniaXamlLoader.Load(this); |
||||
} |
||||
} |
Loading…
Reference in new issue