From 22bdc50f4bd848a6ec17b362ff100282a5d21b4e Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 8 Jul 2023 21:08:05 -0700 Subject: [PATCH] Fix Card control & sorta got model browser going --- StabilityMatrix.Avalonia/App.axaml | 3 + StabilityMatrix.Avalonia/App.axaml.cs | 27 +- StabilityMatrix.Avalonia/Assets/noimage.png | Bin 0 -> 15674 bytes StabilityMatrix.Avalonia/Controls/Card.cs | 19 +- .../DesignData/DesignData.cs | 17 + .../DesignData/MockDownloadService.cs | 6 + .../StabilityMatrix.Avalonia.csproj | 6 +- StabilityMatrix.Avalonia/Styles/Card.axaml | 22 + .../CheckpointBrowserCardViewModel.cs | 269 +++++++++++++ .../ViewModels/CheckpointBrowserViewModel.cs | 381 ++++++++++++++++++ .../Views/CheckpointBrowserPage.axaml | 260 ++++++++++++ .../Views/CheckpointBrowserPage.axaml.cs | 18 + .../Views/CheckpointsPage.axaml | 1 - .../Views/LaunchPageView.axaml | 4 - .../Views/MainWindow.axaml | 4 +- .../Database/LiteDbContext.cs | 2 +- .../Services/DownloadService.cs | 18 +- .../Services/IDownloadService.cs | 2 + 18 files changed, 1026 insertions(+), 33 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Assets/noimage.png create mode 100644 StabilityMatrix.Avalonia/Styles/Card.axaml create mode 100644 StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml create mode 100644 StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index 802657c0..85d7d832 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -3,6 +3,7 @@ x:Class="StabilityMatrix.Avalonia.App" xmlns:local="using:StabilityMatrix.Avalonia" xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia" + RequestedThemeVariant="Default"> @@ -21,6 +22,8 @@ + + diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 356e06b9..1f3da218 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -25,9 +25,12 @@ using Refit; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Factory; +using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; @@ -84,7 +87,9 @@ public partial class App : Application services.AddMemoryCache(); services.AddSingleton(); services.AddSingleton(); - + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -110,6 +115,7 @@ public partial class App : Application { provider.GetRequiredService(), provider.GetRequiredService(), + provider.GetRequiredService() }, FooterPages = new List { @@ -120,8 +126,11 @@ public partial class App : Application services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(_ => { var client = new GitHubClient(new ProductHeaderValue("StabilityMatrix")); @@ -179,13 +188,13 @@ public partial class App : Application .AddPolicyHandler(retryPolicy); // Add Refit clients - // services.AddRefitClient(defaultRefitSettings) - // .ConfigureHttpClient(c => - // { - // c.BaseAddress = new Uri("https://civitai.com"); - // c.Timeout = TimeSpan.FromSeconds(15); - // }) - // .AddPolicyHandler(retryPolicy); + services.AddRefitClient(defaultRefitSettings) + .ConfigureHttpClient(c => + { + c.BaseAddress = new Uri("https://civitai.com"); + c.Timeout = TimeSpan.FromSeconds(15); + }) + .AddPolicyHandler(retryPolicy); // Add Refit client managers services.AddHttpClient("A3Client") @@ -201,7 +210,7 @@ public partial class App : Application builder.SetMinimumLevel(LogLevel.Debug); builder.AddNLog(ConfigureLogging()); }); - + return services; } diff --git a/StabilityMatrix.Avalonia/Assets/noimage.png b/StabilityMatrix.Avalonia/Assets/noimage.png new file mode 100644 index 0000000000000000000000000000000000000000..8b3cefed7cc970964eafc4a456d62f4bc786121e GIT binary patch literal 15674 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 sr2OxU_ typeof(Expander); + public Card() { - // Expander /template/ ToggleButton#PART_toggle - var customStyle = new Style(x => - x.OfType().Template().OfType().Name("PART_toggle")); - - customStyle.Setters.Add(new Setter - { - Property = IsVisibleProperty, - Value = false - }); - - Styles.Add(customStyle); + IsExpanded = true; } } diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 0def0393..37a31764 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using AvaloniaEdit.Utils; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; @@ -55,12 +57,26 @@ public static class DesignData sharedFolders, settingsManager, downloadService, modelFinder); SettingsViewModel = new SettingsViewModel(notificationService); + CheckpointBrowserViewModel = + new CheckpointBrowserViewModel(null!, downloadService, settingsManager, null!) + { + ModelCards = new ObservableCollection + { + new(new CivitModel + { + Name = "BB95 Furry Mix", + Description = "A furry mix of BB95", + }, downloadService, settingsManager) + } + }; + MainWindowViewModel = new MainWindowViewModel { Pages = new List { LaunchPageViewModel, PackageManagerViewModel, + CheckpointBrowserViewModel }, FooterPages = new List { @@ -74,4 +90,5 @@ public static class DesignData public static PackageManagerViewModel PackageManagerViewModel { get; } public static CheckpointsPageViewModel CheckpointsPageViewModel { get; } public static SettingsViewModel SettingsViewModel { get; } + public static CheckpointBrowserViewModel CheckpointBrowserViewModel { get; } } diff --git a/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs b/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs index 8d090cd8..419d972e 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading.Tasks; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; @@ -12,4 +13,9 @@ public class MockDownloadService : IDownloadService { return Task.CompletedTask; } + + public Task GetImageStreamFromUrl(string url) + { + return Task.FromResult(new MemoryStream(new byte[24]) as Stream); + } } diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 64a37f09..54c2686c 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -9,7 +9,6 @@ - @@ -51,4 +50,9 @@ + + + + + diff --git a/StabilityMatrix.Avalonia/Styles/Card.axaml b/StabilityMatrix.Avalonia/Styles/Card.axaml new file mode 100644 index 00000000..beb21c46 --- /dev/null +++ b/StabilityMatrix.Avalonia/Styles/Card.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs new file mode 100644 index 00000000..6eae9db1 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs @@ -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(); + + // 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().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(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(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; + }); + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs new file mode 100644 index 00000000..e974cd66 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs @@ -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? 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 AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast(); + public IEnumerable AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast(); + + public IEnumerable AllModelTypes => Enum.GetValues(typeof(CivitModelType)) + .Cast() + .Where(t => t == CivitModelType.All || t.ConvertTo() > 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); + } + + /// + /// Filter predicate for model cards + /// + private bool FilterModelCardsPredicate(object? item) + { + if (item is not CheckpointBrowserCardViewModel card) return false; + return !card.CivitModel.Nsfw || ShowNsfw; + } + + /// + /// Background update task + /// + 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() > 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(); + } + } + + /// + /// Updates model cards using api response object. + /// + private void UpdateModelCards(IEnumerable? models, CivitMetadata? metadata) + { + if (models is null) + { + ModelCards?.Clear(); + } + else + { + var updateCards = models + .Select(model => new CheckpointBrowserCardViewModel(model, + downloadService, settingsManager)); + ModelCards = new ObservableCollection(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? 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; +} diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml new file mode 100644 index 00000000..ac9c74e2 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs new file mode 100644 index 00000000..da746765 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs @@ -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); + } +} \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index a67da341..4078f160 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -8,7 +8,6 @@ xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}" x:CompileBindings="True" - x:DataType="vm:" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="StabilityMatrix.Avalonia.Views.CheckpointsPage"> Welcome to Avalonia! diff --git a/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml b/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml index c79df581..728711a9 100644 --- a/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml +++ b/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml @@ -117,10 +117,6 @@ VerticalScrollBarVisibility="Auto" FontFamily="Cascadia Code,Consolas,Menlo,Monospace" /> - - - -