Browse Source

Fix Card control & sorta got model browser going

pull/55/head
JT 1 year ago
parent
commit
22bdc50f4b
  1. 3
      StabilityMatrix.Avalonia/App.axaml
  2. 23
      StabilityMatrix.Avalonia/App.axaml.cs
  3. BIN
      StabilityMatrix.Avalonia/Assets/noimage.png
  4. 19
      StabilityMatrix.Avalonia/Controls/Card.cs
  5. 17
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  6. 6
      StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs
  7. 6
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  8. 22
      StabilityMatrix.Avalonia/Styles/Card.axaml
  9. 269
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs
  10. 381
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  11. 260
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  12. 18
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs
  13. 1
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  14. 4
      StabilityMatrix.Avalonia/Views/LaunchPageView.axaml
  15. 4
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  16. 2
      StabilityMatrix.Core/Database/LiteDbContext.cs
  17. 18
      StabilityMatrix.Core/Services/DownloadService.cs
  18. 2
      StabilityMatrix.Core/Services/IDownloadService.cs

3
StabilityMatrix.Avalonia/App.axaml

@ -3,6 +3,7 @@
x:Class="StabilityMatrix.Avalonia.App" x:Class="StabilityMatrix.Avalonia.App"
xmlns:local="using:StabilityMatrix.Avalonia" xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia" xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
RequestedThemeVariant="Default"> RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@ -21,6 +22,8 @@
<Application.Styles> <Application.Styles>
<styling:FluentAvaloniaTheme /> <styling:FluentAvaloniaTheme />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml"/> <StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml"/>
<StyleInclude Source="Styles/ProgressRing.axaml"/>
<StyleInclude Source="Styles/ButtonStyles.axaml"/> <StyleInclude Source="Styles/ButtonStyles.axaml"/>
<StyleInclude Source="Styles/Card.axaml"/>
</Application.Styles> </Application.Styles>
</Application> </Application>

23
StabilityMatrix.Avalonia/App.axaml.cs

@ -25,9 +25,12 @@ using Refit;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models.Configs;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
@ -84,6 +87,8 @@ public partial class App : Application
services.AddMemoryCache(); services.AddMemoryCache();
services.AddSingleton<PackageManagerViewModel>(); services.AddSingleton<PackageManagerViewModel>();
services.AddSingleton<SettingsViewModel>(); services.AddSingleton<SettingsViewModel>();
services.AddSingleton<CheckpointBrowserViewModel>();
services.AddSingleton<CheckpointBrowserCardViewModel>();
services.AddSingleton<ISettingsManager, SettingsManager>(); services.AddSingleton<ISettingsManager, SettingsManager>();
services.AddSingleton<IPackageFactory, PackageFactory>(); services.AddSingleton<IPackageFactory, PackageFactory>();
@ -110,6 +115,7 @@ public partial class App : Application
{ {
provider.GetRequiredService<LaunchPageViewModel>(), provider.GetRequiredService<LaunchPageViewModel>(),
provider.GetRequiredService<PackageManagerViewModel>(), provider.GetRequiredService<PackageManagerViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>()
}, },
FooterPages = new List<PageViewModelBase> FooterPages = new List<PageViewModelBase>
{ {
@ -120,8 +126,11 @@ public partial class App : Application
services.AddTransient<LaunchPageView>(); services.AddTransient<LaunchPageView>();
services.AddTransient<PackageManagerPage>(); services.AddTransient<PackageManagerPage>();
services.AddTransient<SettingsPage>(); services.AddTransient<SettingsPage>();
services.AddTransient<CheckpointBrowserPage>();
services.AddSingleton<MainWindow>(); services.AddSingleton<MainWindow>();
services.AddSingleton<ILiteDbContext, LiteDbContext>();
services.AddTransient<IGitHubClient, GitHubClient>(_ => services.AddTransient<IGitHubClient, GitHubClient>(_ =>
{ {
var client = new GitHubClient(new ProductHeaderValue("StabilityMatrix")); var client = new GitHubClient(new ProductHeaderValue("StabilityMatrix"));
@ -179,13 +188,13 @@ public partial class App : Application
.AddPolicyHandler(retryPolicy); .AddPolicyHandler(retryPolicy);
// Add Refit clients // Add Refit clients
// services.AddRefitClient<ICivitApi>(defaultRefitSettings) services.AddRefitClient<ICivitApi>(defaultRefitSettings)
// .ConfigureHttpClient(c => .ConfigureHttpClient(c =>
// { {
// c.BaseAddress = new Uri("https://civitai.com"); c.BaseAddress = new Uri("https://civitai.com");
// c.Timeout = TimeSpan.FromSeconds(15); c.Timeout = TimeSpan.FromSeconds(15);
// }) })
// .AddPolicyHandler(retryPolicy); .AddPolicyHandler(retryPolicy);
// Add Refit client managers // Add Refit client managers
services.AddHttpClient("A3Client") services.AddHttpClient("A3Client")

BIN
StabilityMatrix.Avalonia/Assets/noimage.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

19
StabilityMatrix.Avalonia/Controls/Card.cs

@ -1,23 +1,14 @@
using Avalonia.Controls; using System;
using Avalonia.Controls.Primitives; using Avalonia.Controls;
using Avalonia.Styling;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
public class Card : Expander public class Card : Expander
{ {
public Card() protected override Type StyleKeyOverride => typeof(Expander);
{
// Expander /template/ ToggleButton#PART_toggle
var customStyle = new Style(x =>
x.OfType<Expander>().Template().OfType<ToggleButton>().Name("PART_toggle"));
customStyle.Setters.Add(new Setter public Card()
{ {
Property = IsVisibleProperty, IsExpanded = true;
Value = false
});
Styles.Add(customStyle);
} }
} }

17
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using AvaloniaEdit.Utils; using AvaloniaEdit.Utils;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
@ -55,12 +57,26 @@ public static class DesignData
sharedFolders, settingsManager, downloadService, modelFinder); sharedFolders, settingsManager, downloadService, modelFinder);
SettingsViewModel = new SettingsViewModel(notificationService); SettingsViewModel = new SettingsViewModel(notificationService);
CheckpointBrowserViewModel =
new CheckpointBrowserViewModel(null!, downloadService, settingsManager, null!)
{
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
{
new(new CivitModel
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
}, downloadService, settingsManager)
}
};
MainWindowViewModel = new MainWindowViewModel MainWindowViewModel = new MainWindowViewModel
{ {
Pages = new List<PageViewModelBase> Pages = new List<PageViewModelBase>
{ {
LaunchPageViewModel, LaunchPageViewModel,
PackageManagerViewModel, PackageManagerViewModel,
CheckpointBrowserViewModel
}, },
FooterPages = new List<PageViewModelBase> FooterPages = new List<PageViewModelBase>
{ {
@ -74,4 +90,5 @@ public static class DesignData
public static PackageManagerViewModel PackageManagerViewModel { get; } public static PackageManagerViewModel PackageManagerViewModel { get; }
public static CheckpointsPageViewModel CheckpointsPageViewModel { get; } public static CheckpointsPageViewModel CheckpointsPageViewModel { get; }
public static SettingsViewModel SettingsViewModel { get; } public static SettingsViewModel SettingsViewModel { get; }
public static CheckpointBrowserViewModel CheckpointBrowserViewModel { get; }
} }

6
StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs

@ -1,4 +1,5 @@
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
@ -12,4 +13,9 @@ public class MockDownloadService : IDownloadService
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<Stream> GetImageStreamFromUrl(string url)
{
return Task.FromResult(new MemoryStream(new byte[24]) as Stream);
}
} }

6
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -9,7 +9,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Controls\" />
<AvaloniaResource Include="Assets\**" /> <AvaloniaResource Include="Assets\**" />
<Folder Include="Models\" /> <Folder Include="Models\" />
</ItemGroup> </ItemGroup>
@ -51,4 +50,9 @@
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\Icon.png" /> <AvaloniaResource Include="Assets\Icon.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Resource Include="Assets\noimage.png" />
</ItemGroup>
</Project> </Project>

22
StabilityMatrix.Avalonia/Styles/Card.axaml

@ -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>

269
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<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;
});
}
}

381
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<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;
}

260
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -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>

18
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);
}
}

1
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -8,7 +8,6 @@
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}" d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}"
x:CompileBindings="True" x:CompileBindings="True"
x:DataType="vm:"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StabilityMatrix.Avalonia.Views.CheckpointsPage"> x:Class="StabilityMatrix.Avalonia.Views.CheckpointsPage">
Welcome to Avalonia! Welcome to Avalonia!

4
StabilityMatrix.Avalonia/Views/LaunchPageView.axaml

@ -117,10 +117,6 @@
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace" /> FontFamily="Cascadia Code,Consolas,Menlo,Monospace" />
<controls:Card IsExpanded="True" Grid.Row="1" Header="Header Stuff" Margin="16">
<TextBlock Text="Hello World"></TextBlock>
</controls:Card>
<!--Command="{Binding LaunchWebUiCommand}"--> <!--Command="{Binding LaunchWebUiCommand}"-->
<!--Visibility="{Binding ShowWebUiButton, Converter={StaticResource BoolToVisConverter}}"--> <!--Visibility="{Binding ShowWebUiButton, Converter={StaticResource BoolToVisConverter}}"-->
<Button <Button

4
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -11,8 +11,8 @@
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
x:CompileBindings="True" x:CompileBindings="True"
Icon="/Assets/Icon.ico" Icon="/Assets/Icon.ico"
Width="900" Width="1100"
Height="650" Height="750"
Title="Stability Matrix Avalonia" Title="Stability Matrix Avalonia"
x:Class="StabilityMatrix.Avalonia.Views.MainWindow"> x:Class="StabilityMatrix.Avalonia.Views.MainWindow">

2
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -36,7 +36,7 @@ public class LiteDbContext : ILiteDbContext
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.debugOptions = debugOptions.Value; this.debugOptions = debugOptions.Value;
var connectionString = debugOptions.Value.TempDatabase ? ":temp:" var connectionString = !debugOptions.Value.TempDatabase ? ":temp:"
: $"Filename={Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db")};Mode=Shared"; : $"Filename={Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db")};Mode=Shared";
Database = new LiteDatabaseAsync(connectionString); Database = new LiteDatabaseAsync(connectionString);

18
StabilityMatrix.Core/Services/DownloadService.cs

@ -25,7 +25,7 @@ public class DownloadService : IDownloadService
: httpClientFactory.CreateClient(httpClientName); : httpClientFactory.CreateClient(httpClientName);
client.Timeout = TimeSpan.FromMinutes(10); client.Timeout = TimeSpan.FromMinutes(10);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "1.0")); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "2.0"));
await using var file = new FileStream(downloadPath, FileMode.Create, FileAccess.Write, FileShare.None); await using var file = new FileStream(downloadPath, FileMode.Create, FileAccess.Write, FileShare.None);
long contentLength = 0; long contentLength = 0;
@ -72,4 +72,20 @@ public class DownloadService : IDownloadService
progress?.Report(new ProgressReport(1f, message: "Download complete!")); progress?.Report(new ProgressReport(1f, message: "Download complete!"));
} }
public async Task<Stream> GetImageStreamFromUrl(string url)
{
using var client = httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "2.0"));
try
{
var response = await client.GetAsync(url);
return await response.Content.ReadAsStreamAsync();
}
catch (Exception e)
{
return default;
}
}
} }

2
StabilityMatrix.Core/Services/IDownloadService.cs

@ -6,4 +6,6 @@ public interface IDownloadService
{ {
Task DownloadToFileAsync(string downloadUrl, string downloadPath, Task DownloadToFileAsync(string downloadUrl, string downloadPath,
IProgress<ProgressReport>? progress = null, string? httpClientName = null); IProgress<ProgressReport>? progress = null, string? httpClientName = null);
Task<Stream> GetImageStreamFromUrl(string url);
} }

Loading…
Cancel
Save