Browse Source

tabs & editable page number

pull/324/head
JT 12 months ago
parent
commit
3c39fe3337
  1. 202
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  2. 13
      StabilityMatrix.Avalonia/ViewModels/Base/TabViewModelBase.cs
  3. 611
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  4. 37
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/HuggingFacePageViewModel.cs
  5. 654
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  6. 549
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  7. 29
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs
  8. 536
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml
  9. 41
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs
  10. 37
      StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml

202
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -21,9 +21,9 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
@ -41,6 +41,8 @@ using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel;
using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.HuggingFacePageViewModel;
namespace StabilityMatrix.Avalonia.DesignData;
@ -75,10 +77,7 @@ public static class DesignData
Id = activePackageId,
DisplayName = "My Installed Package",
PackageName = "stable-diffusion-webui",
Version = new InstalledPackageVersion
{
InstalledReleaseVersion = "v1.0.0"
},
Version = new InstalledPackageVersion { InstalledReleaseVersion = "v1.0.0" },
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
},
@ -90,8 +89,7 @@ public static class DesignData
Version = new InstalledPackageVersion
{
InstalledBranch = "master",
InstalledCommitSha =
"abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8"
},
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
@ -209,14 +207,10 @@ public static class DesignData
Fp = CivitModelFpType.fp16,
Size = CivitModelSize.pruned,
},
TrainedWords = ["aurora", "lightning"]
TrainedWords = ["aurora", "lightning"]
}
},
new()
{
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
},
new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" },
},
},
new(settingsManager, downloadService, modelFinder, notificationService, modelImportService)
@ -300,35 +294,33 @@ public static class DesignData
);
}*/
CheckpointBrowserViewModel.ModelCards =
new ObservableCollection<CheckpointBrowserCardViewModel>
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
vm.CivitModel = new CivitModel
{
vm.CivitModel = new CivitModel
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
ModelVersions = [new() { Name = "v1.2.2-Inpainting" }],
Creator = new CivitCreator
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
ModelVersions = [
new() { Name = "v1.2.2-Inpainting" }
],
Creator = new CivitCreator
{
Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro",
Username = "creator-1"
}
};
}),
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro",
Username = "creator-1"
}
};
}),
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = new CivitModel
{
vm.CivitModel = new CivitModel
{
Name = "Another Model",
Description = "A mix of example",
Stats = new CivitModelStats { Rating = 5, RatingCount = 3500 },
ModelVersions = [
new()
Name = "Another Model",
Description = "A mix of example",
Stats = new CivitModelStats { Rating = 5, RatingCount = 3500 },
ModelVersions =
[
new()
{
Name = "v1.2.2-Inpainting",
Images = new List<CivitImage>
@ -340,16 +332,16 @@ public static class DesignData
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
}
}
}
],
Creator = new CivitCreator
{
Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro",
Username = "creator-2"
}
};
})
};
}
],
Creator = new CivitCreator
{
Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro",
Username = "creator-2"
}
};
})
};
NewCheckpointsPageViewModel.AllCheckpoints = new ObservableCollection<CheckpointFile>
{
@ -377,33 +369,30 @@ public static class DesignData
new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }
};
ProgressManagerViewModel.ProgressItems.AddRange(
new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(
new ProgressItem(
Guid.NewGuid(),
"Test File.exe",
new ProgressReport(0.5f, "Downloading...")
ProgressManagerViewModel
.ProgressItems
.AddRange(
new ProgressItemViewModelBase[]
{
new ProgressItemViewModel(
new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))
),
new MockDownloadProgressItemViewModel(
"Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe"
),
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
)
),
new MockDownloadProgressItemViewModel(
"Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe"
),
new PackageInstallProgressItemViewModel(
new PackageModificationRunner
{
CurrentProgress = new ProgressReport(0.5f, "Installing package...")
}
)
}
);
}
);
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.CurrentVersionText = "v2.0.0";
UpdateViewModel.NewVersionText = "v2.0.1";
UpdateViewModel.ReleaseNotes =
"## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature";
UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature";
isInitialized = true;
}
@ -420,15 +409,13 @@ public static class DesignData
public static ServiceManager<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>();
public static MainWindowViewModel MainWindowViewModel =>
Services.GetRequiredService<MainWindowViewModel>();
public static MainWindowViewModel MainWindowViewModel => Services.GetRequiredService<MainWindowViewModel>();
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
Services.GetRequiredService<FirstLaunchSetupViewModel>();
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService<LaunchPageViewModel>();
public static HuggingFacePageViewModel HuggingFacePageViewModel =>
Services.GetRequiredService<HuggingFacePageViewModel>();
@ -461,10 +448,7 @@ public static class DesignData
vm.SetPackages(settings.Settings.InstalledPackages);
vm.SetUnknownPackages(
new InstalledPackage[]
{
UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"),
}
new InstalledPackage[] { UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), }
);
vm.PackageCards[0].IsUpdateAvailable = true;
@ -479,14 +463,12 @@ public static class DesignData
public static NewCheckpointsPageViewModel NewCheckpointsPageViewModel =>
Services.GetRequiredService<NewCheckpointsPageViewModel>();
public static SettingsViewModel SettingsViewModel =>
Services.GetRequiredService<SettingsViewModel>();
public static SettingsViewModel SettingsViewModel => Services.GetRequiredService<SettingsViewModel>();
public static InferenceSettingsViewModel InferenceSettingsViewModel =>
Services.GetRequiredService<InferenceSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel =>
Services.GetRequiredService<MainSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel => Services.GetRequiredService<MainSettingsViewModel>();
public static AccountSettingsViewModel AccountSettingsViewModel =>
Services.GetRequiredService<AccountSettingsViewModel>();
@ -506,7 +488,7 @@ public static class DesignData
HashBlake3 = "",
Signature = "",
};
vm.UpdateStatus = new UpdateStatusChangedEventArgs
{
LatestUpdate = update,
@ -522,6 +504,9 @@ public static class DesignData
}
}
public static CivitAiBrowserViewModel CivitAiBrowserViewModel =>
Services.GetRequiredService<CivitAiBrowserViewModel>();
public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService<CheckpointBrowserViewModel>();
@ -567,10 +552,7 @@ The gallery images are often inpainted, but you will get something very similar
}
}
};
var sampleViewModel = new ModelVersionViewModel(
new HashSet<string> { "ABCD" },
sampleCivitVersions[0]
);
var sampleViewModel = new ModelVersionViewModel(new HashSet<string> { "ABCD" }, sampleCivitVersions[0]);
// Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel> { sampleViewModel };
@ -582,8 +564,7 @@ The gallery images are often inpainted, but you will get something very similar
public static OneClickInstallViewModel OneClickInstallViewModel =>
Services.GetRequiredService<OneClickInstallViewModel>();
public static InferenceViewModel InferenceViewModel =>
Services.GetRequiredService<InferenceViewModel>();
public static InferenceViewModel InferenceViewModel => Services.GetRequiredService<InferenceViewModel>();
public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel =>
Services.GetRequiredService<SelectDataDirectoryViewModel>();
@ -621,14 +602,10 @@ The gallery images are often inpainted, but you will get something very similar
public static PythonPackagesViewModel PythonPackagesViewModel =>
DialogFactory.Get<PythonPackagesViewModel>(vm =>
{
vm.AddPackages(
new PipPackageInfo("pip", "1.0.0"),
new PipPackageInfo("torch", "2.1.0+cu121")
);
vm.AddPackages(new PipPackageInfo("pip", "1.0.0"), new PipPackageInfo("torch", "2.1.0+cu121"));
});
public static LykosLoginViewModel LykosLoginViewModel =>
DialogFactory.Get<LykosLoginViewModel>();
public static LykosLoginViewModel LykosLoginViewModel => DialogFactory.Get<LykosLoginViewModel>();
public static OAuthConnectViewModel OAuthConnectViewModel =>
DialogFactory.Get<OAuthConnectViewModel>(vm =>
@ -652,11 +629,9 @@ The gallery images are often inpainted, but you will get something very similar
public static InferenceImageUpscaleViewModel InferenceImageUpscaleViewModel =>
DialogFactory.Get<InferenceImageUpscaleViewModel>();
public static PackageImportViewModel PackageImportViewModel =>
DialogFactory.Get<PackageImportViewModel>();
public static PackageImportViewModel PackageImportViewModel => DialogFactory.Get<PackageImportViewModel>();
public static RefreshBadgeViewModel RefreshBadgeViewModel =>
new() { State = ProgressState.Success };
public static RefreshBadgeViewModel RefreshBadgeViewModel => new() { State = ProgressState.Success };
public static SeedCardViewModel SeedCardViewModel => new();
@ -713,8 +688,7 @@ The gallery images are often inpainted, but you will get something very similar
);
});
public static ImageFolderCardViewModel ImageFolderCardViewModel =>
DialogFactory.Get<ImageFolderCardViewModel>();
public static ImageFolderCardViewModel ImageFolderCardViewModel => DialogFactory.Get<ImageFolderCardViewModel>();
public static FreeUCardViewModel FreeUCardViewModel => DialogFactory.Get<FreeUCardViewModel>();
@ -746,16 +720,12 @@ The gallery images are often inpainted, but you will get something very similar
DialogFactory.Get<StackExpanderViewModel>(vm =>
{
vm.Title = "Hires Fix";
vm.AddCards(
new LoadableViewModelBase[] { UpscalerCardViewModel, SamplerCardViewModel }
);
vm.AddCards(new LoadableViewModelBase[] { UpscalerCardViewModel, SamplerCardViewModel });
});
public static UpscalerCardViewModel UpscalerCardViewModel =>
DialogFactory.Get<UpscalerCardViewModel>();
public static UpscalerCardViewModel UpscalerCardViewModel => DialogFactory.Get<UpscalerCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel =>
DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModel => DialogFactory.Get<BatchSizeCardViewModel>();
public static BatchSizeCardViewModel BatchSizeCardViewModelWithIndexOption =>
DialogFactory.Get<BatchSizeCardViewModel>(vm =>
@ -807,14 +777,12 @@ The gallery images are often inpainted, but you will get something very similar
vm.Resource = ComfyUpscaler.DefaultDownloadableModels[0].DownloadableResource!.Value;
});
public static SharpenCardViewModel SharpenCardViewModel =>
DialogFactory.Get<SharpenCardViewModel>();
public static SharpenCardViewModel SharpenCardViewModel => DialogFactory.Get<SharpenCardViewModel>();
public static InferenceConnectionHelpViewModel InferenceConnectionHelpViewModel =>
DialogFactory.Get<InferenceConnectionHelpViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel =>
DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel => DialogFactory.Get<SelectImageCardViewModel>();
public static SelectImageCardViewModel SelectImageCardViewModel_WithImage =>
DialogFactory.Get<SelectImageCardViewModel>(vm =>
@ -828,9 +796,7 @@ The gallery images are often inpainted, but you will get something very similar
public static ImageSource SampleImageSource =>
new(
new Uri(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500"
)
new Uri("https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500")
);
public static Indexer Types { get; } = new();
@ -841,9 +807,7 @@ The gallery images are often inpainted, but you will get something very similar
{
get
{
var type =
Type.GetType(typeName)
?? throw new ArgumentException($"Type {typeName} not found");
var type = Type.GetType(typeName) ?? throw new ArgumentException($"Type {typeName} not found");
try
{
return Services.GetService(type);

13
StabilityMatrix.Avalonia/ViewModels/Base/TabViewModelBase.cs

@ -0,0 +1,13 @@
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public abstract partial class TabViewModelBase : ViewModelBase
{
[ObservableProperty]
private List<ICommandBarElement> primaryCommands = new();
public abstract string Header { get; }
}

611
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs

@ -0,0 +1,611 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LiteDB;
using LiteDB.Async;
using NLog;
using Refit;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
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.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[View(typeof(CivitAiBrowserPage))]
[Singleton]
public partial class CivitAiBrowserViewModel : TabViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 20;
private LRUCache<
int /* model id */
,
CheckpointBrowserCardViewModel
> cache = new(50);
[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 canGoToFirstPage;
[ObservableProperty]
private bool canGoToLastPage;
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
private bool noResultsFound;
[ObservableProperty]
private string noResultsText = string.Empty;
[ObservableProperty]
private string selectedBaseModelType = "All";
private List<CheckpointBrowserCardViewModel> allModelCards = new();
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 List<string> BaseModelOptions => new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" };
public CivitAiBrowserViewModel(
ICivitApi civitApi,
IDownloadService downloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
ILiteDbContext liteDbContext,
INotificationService notificationService
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext;
this.notificationService = notificationService;
CurrentPageNumber = 1;
CanGoToNextPage = true;
CanGoToLastPage = true;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select<EventPattern<PropertyChangedEventArgs>, int>(_ => CurrentPageNumber)
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
var searchOptions = settingsManager.Settings.ModelSearchOptions;
// Fix SelectedModelType if someone had selected the obsolete "Model" option
if (searchOptions is { SelectedModelType: CivitModelType.Model })
{
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
CivitModelType.Checkpoint,
SelectedBaseModelType
)
);
searchOptions = settingsManager.Settings.ModelSearchOptions;
}
SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month;
SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated;
SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint;
SelectedBaseModelType = searchOptions?.SelectedBaseModelType ?? "All";
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)
{
notificationService.Show(
new Notification("Request to CivitAI timed out", "Please try again in a few minutes")
);
Logger.Warn($"CivitAI query timed out ({request})");
}
catch (HttpRequestException e)
{
notificationService.Show(
new Notification("CivitAI can't be reached right now", "Please try again in a few minutes")
);
Logger.Warn(e, $"CivitAI query HttpRequestException ({request})");
}
catch (ApiException e)
{
notificationService.Show(
new Notification("CivitAI can't be reached right now", "Please try again in a few minutes")
);
Logger.Warn(e, $"CivitAI query ApiException ({request})");
}
catch (Exception e)
{
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
$"Unknown exception during CivitAI query: {e.GetType().Name}"
)
);
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 =>
{
var cachedViewModel = cache.Get(model.Id);
if (cachedViewModel != null)
{
if (!cachedViewModel.IsImporting)
{
cache.Remove(model.Id);
}
return cachedViewModel;
}
var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{
if (cache.Get(viewModel.CivitModel.Id) != null)
return;
cache.Add(viewModel.CivitModel.Id, viewModel);
};
return vm;
});
return newCard;
})
.ToList();
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed)
{
filteredCards = filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available");
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
}
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
CanGoToLastPage = 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 };
}
if (SelectedBaseModelType != "All")
{
modelRequest.BaseModel = SelectedBaseModelType;
}
if (SortMode == CivitSortMode.Installed)
{
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
modelRequest.CommaSeparatedModelIds = string.Join(
",",
connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m).Select(g => g.First())
);
modelRequest.Sort = null;
modelRequest.Period = null;
}
else if (SortMode == CivitSortMode.Favorites)
{
var favoriteModels = settingsManager.Settings.FavoriteModels;
if (!favoriteModels.Any())
{
notificationService.Show(
"No Favorites",
"You have not added any models to your Favorites.",
NotificationType.Error
);
return;
}
modelRequest.CommaSeparatedModelIds = string.Join(",", favoriteModels);
modelRequest.Sort = null;
modelRequest.Period = null;
}
// See if query is cached
CivitModelQueryCacheEntry? cachedQuery = null;
try
{
cachedQuery = await liteDbContext
.CivitModelQueryCache
.IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
}
catch (Exception e)
{
// Suppress 'Training_Data' enum not found exceptions
// Caused by enum name change
// Ignore to do a new search to overwrite the cache
if (
!(
e is LiteException or LiteAsyncException
&& e.InnerException is ArgumentException inner
&& inner.Message.Contains("Training_Data")
)
)
{
// Otherwise log error
Logger.Error(e, "Error while querying CivitModelQueryCache");
}
}
// 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();
}
public void FirstPage()
{
CurrentPageNumber = 1;
}
public void PreviousPage()
{
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
public void LastPage()
{
CurrentPageNumber = TotalPages;
}
public void ClearSearchQuery()
{
SearchQuery = string.Empty;
}
partial void OnShowNsfwChanged(bool value)
{
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value);
// ModelCardsView?.Refresh();
var updateCards = allModelCards.Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);
if (!HasSearched)
return;
UpdateResultsText();
}
partial void OnSelectedPeriodChanged(CivitPeriod value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(value, SortMode, SelectedModelType, SelectedBaseModelType)
);
}
partial void OnSortModeChanged(CivitSortMode value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
value,
SelectedModelType,
SelectedBaseModelType
)
);
}
partial void OnSelectedModelTypeChanged(CivitModelType value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s => s.ModelSearchOptions = new ModelSearchOptions(SelectedPeriod, SortMode, value, SelectedBaseModelType)
);
}
partial void OnSelectedBaseModelTypeChanged(string value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s => s.ModelSearchOptions = new ModelSearchOptions(SelectedPeriod, SortMode, SelectedModelType, value)
);
}
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;
NoResultsText =
allModelCards.Count > 0 ? $"{allModelCards.Count} results hidden by filters" : "No results found";
}
public override string Header => "Civitai";
}

37
StabilityMatrix.Avalonia/ViewModels/HuggingFacePageViewModel.cs → StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/HuggingFacePageViewModel.cs

@ -3,16 +3,23 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models.HuggingFace;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
@ -24,19 +31,16 @@ using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[View(typeof(Views.HuggingFacePage))]
[Singleton]
public partial class HuggingFacePageViewModel : PageViewModelBase
public partial class HuggingFacePageViewModel : TabViewModelBase
{
private readonly ITrackedDownloadService trackedDownloadService;
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
public override string Title => "Hugging Face";
public override IconSource IconSource =>
new FASymbolIconSource { Symbol = "fa-solid fa-file-code" };
public SourceCache<HuggingfaceItem, string> ItemsCache { get; } =
new(i => i.RepositoryPath + i.ModelName);
@ -98,10 +102,7 @@ public partial class HuggingFacePageViewModel : PageViewModelBase
TotalProgress = new ProgressReport(current: currentSum, total: totalSum);
};
}
public override void OnLoaded()
{
using var reader = new StreamReader(Assets.HfPackagesJson.Open());
var packages =
JsonSerializer.Deserialize<IReadOnlyList<HuggingfaceItem>>(
@ -110,6 +111,15 @@ public partial class HuggingFacePageViewModel : PageViewModelBase
) ?? throw new InvalidOperationException("Failed to read hf-packages.json");
ItemsCache.EditDiff(packages, (a, b) => a.RepositoryPath == b.RepositoryPath);
var btn = new CommandBarButton();
var obs = btn.GetObservable(InputElement.IsEnabledProperty)
.Select(_ => NumSelected > 0);
btn.Bind(InputElement.IsEnabledProperty, obs);
btn.Command = ImportSelectedCommand;
btn.Foreground = new SolidColorBrush(Colors.Lime);
btn.IconSource = new SymbolIconSource { Symbol = Symbol.Download };
PrimaryCommands.Add(btn);
}
public void ClearSelection()
@ -129,7 +139,8 @@ public partial class HuggingFacePageViewModel : PageViewModelBase
}
}
public async Task ImportSelected()
[RelayCommand]
private async Task ImportSelected()
{
var selected = Categories.SelectMany(c => c.Items).Where(i => i.IsSelected).ToArray();
@ -189,4 +200,6 @@ public partial class HuggingFacePageViewModel : PageViewModelBase
TotalProgress = new ProgressReport(0, 0);
});
}
public override string Header => "Hugging Face";
}

654
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -1,40 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using LiteDB;
using LiteDB.Async;
using NLog;
using Refit;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
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.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -42,621 +15,18 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(CheckpointBrowserPage))]
[Singleton]
public partial class CheckpointBrowserViewModel : PageViewModelBase
public partial class CheckpointBrowserViewModel(
CivitAiBrowserViewModel civitAiBrowserViewModel,
HuggingFacePageViewModel huggingFaceViewModel
) : PageViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 20;
private LRUCache<
int /* model id */
,
CheckpointBrowserCardViewModel
> cache = new(50);
[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 canGoToFirstPage;
[ObservableProperty]
private bool canGoToLastPage;
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
private bool noResultsFound;
[ObservableProperty]
private string noResultsText = string.Empty;
[ObservableProperty]
private string selectedBaseModelType = "All";
private List<CheckpointBrowserCardViewModel> allModelCards = new();
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 List<string> BaseModelOptions =>
new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" };
public CheckpointBrowserViewModel(
ICivitApi civitApi,
IDownloadService downloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
ILiteDbContext liteDbContext,
INotificationService notificationService
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext;
this.notificationService = notificationService;
CurrentPageNumber = 1;
CanGoToNextPage = true;
CanGoToLastPage = true;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select(_ => CurrentPageNumber)
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
var searchOptions = settingsManager.Settings.ModelSearchOptions;
// Fix SelectedModelType if someone had selected the obsolete "Model" option
if (searchOptions is { SelectedModelType: CivitModelType.Model })
{
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
CivitModelType.Checkpoint,
SelectedBaseModelType
)
);
searchOptions = settingsManager.Settings.ModelSearchOptions;
}
SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month;
SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated;
SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint;
SelectedBaseModelType = searchOptions?.SelectedBaseModelType ?? "All";
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)
{
notificationService.Show(
new Notification(
"Request to CivitAI timed out",
"Please try again in a few minutes"
)
);
Logger.Warn($"CivitAI query timed out ({request})");
}
catch (HttpRequestException e)
{
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
"Please try again in a few minutes"
)
);
Logger.Warn(e, $"CivitAI query HttpRequestException ({request})");
}
catch (ApiException e)
{
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
"Please try again in a few minutes"
)
);
Logger.Warn(e, $"CivitAI query ApiException ({request})");
}
catch (Exception e)
{
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
$"Unknown exception during CivitAI query: {e.GetType().Name}"
)
);
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 =>
{
var cachedViewModel = cache.Get(model.Id);
if (cachedViewModel != null)
{
if (!cachedViewModel.IsImporting)
{
cache.Remove(model.Id);
}
return cachedViewModel;
}
var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{
if (cache.Get(viewModel.CivitModel.Id) != null)
return;
cache.Add(viewModel.CivitModel.Id, viewModel);
};
return vm;
});
return newCard;
})
.ToList();
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed)
{
filteredCards = filteredCards.OrderByDescending(
x => x.UpdateCardText == "Update Available"
);
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
}
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
CanGoToLastPage = 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 };
}
if (SelectedBaseModelType != "All")
{
modelRequest.BaseModel = SelectedBaseModelType;
}
if (SortMode == CivitSortMode.Installed)
{
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
modelRequest.CommaSeparatedModelIds = string.Join(
",",
connectedModels
.Select(c => c.ConnectedModel!.ModelId)
.GroupBy(m => m)
.Select(g => g.First())
);
modelRequest.Sort = null;
modelRequest.Period = null;
}
else if (SortMode == CivitSortMode.Favorites)
{
var favoriteModels = settingsManager.Settings.FavoriteModels;
if (!favoriteModels.Any())
{
notificationService.Show(
"No Favorites",
"You have not added any models to your Favorites.",
NotificationType.Error
);
return;
}
modelRequest.CommaSeparatedModelIds = string.Join(",", favoriteModels);
modelRequest.Sort = null;
modelRequest.Period = null;
}
// See if query is cached
CivitModelQueryCacheEntry? cachedQuery = null;
public override string Title => "Model Browser";
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };
try
{
cachedQuery = await liteDbContext.CivitModelQueryCache
.IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
}
catch (Exception e)
{
// Suppress 'Training_Data' enum not found exceptions
// Caused by enum name change
// Ignore to do a new search to overwrite the cache
if (
!(
e is LiteException or LiteAsyncException
&& e.InnerException is ArgumentException inner
&& inner.Message.Contains("Training_Data")
)
public IReadOnlyList<TabItem> Pages { get; } =
new List<TabItem>(
new List<TabViewModelBase>([civitAiBrowserViewModel, huggingFaceViewModel]).Select(
vm => new TabItem { Header = vm.Header, Content = vm }
)
{
// Otherwise log error
Logger.Error(e, "Error while querying CivitModelQueryCache");
}
}
// 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();
}
public void FirstPage()
{
CurrentPageNumber = 1;
}
public void PreviousPage()
{
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
public void LastPage()
{
CurrentPageNumber = TotalPages;
}
public void ClearSearchQuery()
{
SearchQuery = string.Empty;
}
partial void OnShowNsfwChanged(bool value)
{
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value);
// ModelCardsView?.Refresh();
var updateCards = allModelCards.Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);
if (!HasSearched)
return;
UpdateResultsText();
}
partial void OnSelectedPeriodChanged(CivitPeriod value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
value,
SortMode,
SelectedModelType,
SelectedBaseModelType
)
);
}
partial void OnSortModeChanged(CivitSortMode value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
value,
SelectedModelType,
SelectedBaseModelType
)
);
}
partial void OnSelectedModelTypeChanged(CivitModelType value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
value,
SelectedBaseModelType
)
);
}
partial void OnSelectedBaseModelTypeChanged(string value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
SelectedModelType,
value
)
);
}
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;
NoResultsText =
allModelCards.Count > 0
? $"{allModelCards.Count} results hidden by filters"
: "No results found";
}
public override string Title => "Model Browser";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };
}

549
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -1,531 +1,18 @@
<UserControl
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="viewModels:CheckpointBrowserViewModel"
mc:Ignorable="d">
<UserControl.Styles>
<Style Selector="Border#HoverBorder">
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237"/>
</Transitions>
</Setter>
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.237">
<TransformOperationsTransition.Easing>
<QuadraticEaseInOut/>
</TransformOperationsTransition.Easing>
</TransformOperationsTransition>
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/>
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#CC000000" />
</Style>
</Style>
<Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/>
<Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="RenderTransform" Value="scale(1, 1)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#99000000" />
</Style>
</Style>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}"
Command="{Binding OpenModelCommand}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<Button
Name="ModelCard"
Classes="transparent-full"
Padding="0"
BorderThickness="0"
VerticalContentAlignment="Top"
CornerRadius="8"
Command="{Binding ShowVersionDialogCommand}"
CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}">
<Grid RowDefinitions="*, Auto">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding CardImage}"
Stretch="UniformToFill"
StretchDirection="Both"/>
<StackPanel
Grid.Row="0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="transparent-info"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding !IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarAdd" />
</Grid>
</Button>
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="success"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarFilled" />
</Grid>
</Button>
</StackPanel>
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9"/>
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2"/>
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<controls:BetterAdvancedImage
Width="22"
Height="22"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding CivitModel.Creator.Image}"/>
<TextBlock
VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding CivitModel.Creator.Username}"/>
</StackPanel>
</Button>
</Border>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<controls:Card
Height="24"
Margin="8,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.Type}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.BaseModelType}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="success"
IsVisible="{Binding ShowUpdateCard}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding UpdateCardText}" />
</controls:Card>
</StackPanel>
<Border
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="#DD000000"
CornerRadius="8"
IsVisible="{Binding IsImporting}"
ZIndex="1" />
<StackPanel
Grid.Row="0"
Grid.RowSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding IsImporting}"
Orientation="Vertical"
ZIndex="2">
<controls:ProgressRing
Width="120"
Height="120"
HorizontalAlignment="Center"
VerticalAlignment="Center"
EndAngle="450"
IsIndeterminate="False"
StartAngle="90"
Value="{Binding Value}" />
<TextBlock
Margin="0,8,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Text, TargetNullValue=Importing...}" />
</StackPanel>
<Border
Name="ModelCardBottom"
Grid.Row="1">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto">
<!--
TextTrimming causing issues with unicode chars until
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released
-->
<TextBlock
Grid.ColumnSpan="2"
MaxWidth="250"
Margin="8,0,8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource TextControlForeground}"
LetterSpacing="0.33"
Text="{Binding CivitModel.Name}"
TextWrapping="NoWrap"
ToolTip.Tip="{Binding CivitModel.Name}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="8,-4,0,0"
VerticalAlignment="Center"
FontSize="11"
Foreground="{DynamicResource TextControlForeground}"
Text="{Binding CivitModel.LatestModelVersionName, FallbackValue=''}" />
<StackPanel
Grid.Row="2"
Grid.Column="0"
Orientation="Horizontal">
<controls:StarsRating
Margin="8,8,0,8"
Background="#66000000"
FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}"
TextAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<avalonia:Icon Value="fa-solid fa-heart" />
<TextBlock
Margin="4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
Margin="0,0,4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.DownloadCount, Converter={StaticResource KiloFormatterConverter}}" />
</StackPanel>
<Button
Grid.Row="0"
Grid.Column="1"
Width="32"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{Binding OpenModelCommand}" Header="{x:Static lang:Resources.Action_OpenOnCivitAi}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Border>
</Grid>
</Button>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid Margin="0,8,0,0" RowDefinitions="Auto,*,Auto">
<StackPanel Margin="8" Orientation="Vertical">
<Grid ColumnDefinitions="*,Auto">
<TextBox
Margin="8,0,0,0"
HorizontalAlignment="Stretch"
KeyDown="InputElement_OnKeyDown"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}">
<TextBox.InnerRightContent>
<Button
Classes="transparent-full"
Command="{Binding ClearSearchQuery}"
IsVisible="{Binding SearchQuery.Length}">
<ui:SymbolIcon Symbol="Cancel" />
</Button>
</TextBox.InnerRightContent>
</TextBox>
<Button
Grid.Column="1"
Width="80"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchModelsCommand}"
IsDefault="True">
<Grid>
<controls:ProgressRing
MinWidth="16"
MinHeight="16"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding SearchModelsCommand.IsRunning}" />
<TextBlock
VerticalAlignment="Center"
IsVisible="{Binding !SearchModelsCommand.IsRunning}"
Text="{x:Static lang:Resources.Action_Search}" />
</Grid>
</Button>
</Grid>
<DockPanel>
<StackPanel Margin="8,8,4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllSortModes}"
SelectedItem="{Binding SortMode}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllCivitPeriods}"
SelectedItem="{Binding SelectedPeriod}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_ModelType}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllModelTypes}"
SelectedItem="{Binding SelectedModelType}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_BaseModel}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding BaseModelOptions}"
SelectedItem="{Binding SelectedBaseModelType}" />
</StackPanel>
<CheckBox
Margin="8,8,8,0"
HorizontalAlignment="Right"
Content="{x:Static lang:Resources.Label_ShowNsfwContent}"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" />
</DockPanel>
</StackPanel>
<ScrollViewer
Grid.Row="1"
Margin="8,0,8,0"
ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}"
HorizontalAlignment="Center"
ItemsSource="{Binding ModelCards}">
<ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<TextBlock
Grid.Row="2"
Margin="16,8"
VerticalAlignment="Bottom"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" />
<StackPanel
Grid.Row="2"
Margin="8"
HorizontalAlignment="Center"
IsVisible="{Binding HasSearched}"
Orientation="Vertical">
<TextBlock Margin="0,0,4,4" TextAlignment="Center">
<Run Text="{x:Static lang:Resources.Label_Page}" />
<Run Text="{Binding CurrentPageNumber, FallbackValue=1}" />
<Run Text="/" />
<Run Text="{Binding TotalPages, FallbackValue=5}" />
</TextBlock>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,8,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<Button
Margin="0,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
</StackPanel>
<TextBlock
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
IsVisible="{Binding NoResultsFound}"
Text="{Binding NoResultsText, FallbackValue=No results found}" />
<controls:ProgressRing
Grid.Row="1"
Width="128"
Height="128"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsIndeterminate="True"
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" />
</Grid>
</UserControl>
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="viewModels:CheckpointBrowserViewModel"
mc:Ignorable="d"
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage">
<TabControl ItemsSource="{Binding Pages}"/>
</controls:UserControlBase>

29
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs

@ -1,9 +1,4 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views;
@ -15,26 +10,4 @@ public partial class CheckpointBrowserPage : UserControlBase
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (sender is not ScrollViewer scrollViewer)
return;
var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum;
Debug.WriteLine($"IsAtEnd: {isAtEnd}");
}
private void InputElement_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape && DataContext is CheckpointBrowserViewModel viewModel)
{
viewModel.ClearSearchQuery();
}
}
}

536
StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml

@ -0,0 +1,536 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.CivitAiBrowserPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="checkpointBrowser:CivitAiBrowserViewModel"
mc:Ignorable="d">
<UserControl.Styles>
<Style Selector="Border#HoverBorder">
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237"/>
</Transitions>
</Setter>
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.237">
<TransformOperationsTransition.Easing>
<QuadraticEaseInOut/>
</TransformOperationsTransition.Easing>
</TransformOperationsTransition>
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/>
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#CC000000" />
</Style>
</Style>
<Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/>
<Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="RenderTransform" Value="scale(1, 1)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#99000000" />
</Style>
</Style>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}"
Command="{Binding OpenModelCommand}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<Button
Name="ModelCard"
Classes="transparent-full"
Padding="0"
BorderThickness="0"
VerticalContentAlignment="Top"
CornerRadius="8"
Command="{Binding ShowVersionDialogCommand}"
CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}">
<Grid RowDefinitions="*, Auto">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding CardImage}"
Stretch="UniformToFill"
StretchDirection="Both"/>
<StackPanel
Grid.Row="0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="transparent-info"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding !IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarAdd" />
</Grid>
</Button>
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="success"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarFilled" />
</Grid>
</Button>
</StackPanel>
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9"/>
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2"/>
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<controls:BetterAdvancedImage
Width="22"
Height="22"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding CivitModel.Creator.Image}"/>
<TextBlock
VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding CivitModel.Creator.Username}"/>
</StackPanel>
</Button>
</Border>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<controls:Card
Height="24"
Margin="8,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.Type}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.BaseModelType}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="success"
IsVisible="{Binding ShowUpdateCard}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding UpdateCardText}" />
</controls:Card>
</StackPanel>
<Border
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="#DD000000"
CornerRadius="8"
IsVisible="{Binding IsImporting}"
ZIndex="1" />
<StackPanel
Grid.Row="0"
Grid.RowSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding IsImporting}"
Orientation="Vertical"
ZIndex="2">
<controls:ProgressRing
Width="120"
Height="120"
HorizontalAlignment="Center"
VerticalAlignment="Center"
EndAngle="450"
IsIndeterminate="False"
StartAngle="90"
Value="{Binding Value}" />
<TextBlock
Margin="0,8,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Text, TargetNullValue=Importing...}" />
</StackPanel>
<Border
Name="ModelCardBottom"
Grid.Row="1">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto">
<!--
TextTrimming causing issues with unicode chars until
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released
-->
<TextBlock
Grid.ColumnSpan="2"
MaxWidth="250"
Margin="8,0,8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource TextControlForeground}"
LetterSpacing="0.33"
Text="{Binding CivitModel.Name}"
TextWrapping="NoWrap"
ToolTip.Tip="{Binding CivitModel.Name}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="8,-4,0,0"
VerticalAlignment="Center"
FontSize="11"
Foreground="{DynamicResource TextControlForeground}"
Text="{Binding CivitModel.LatestModelVersionName, FallbackValue=''}" />
<StackPanel
Grid.Row="2"
Grid.Column="0"
Orientation="Horizontal">
<controls:StarsRating
Margin="8,8,0,8"
Background="#66000000"
FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}"
TextAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<avalonia:Icon Value="fa-solid fa-heart" />
<TextBlock
Margin="4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
Margin="0,0,4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.DownloadCount, Converter={StaticResource KiloFormatterConverter}}" />
</StackPanel>
<Button
Grid.Row="0"
Grid.Column="1"
Width="32"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{Binding OpenModelCommand}" Header="{x:Static lang:Resources.Action_OpenOnCivitAi}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Border>
</Grid>
</Button>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid RowDefinitions="Auto,*,Auto">
<StackPanel Margin="8" Orientation="Vertical">
<Grid ColumnDefinitions="*,Auto">
<TextBox
HorizontalAlignment="Stretch"
KeyDown="InputElement_OnKeyDown"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}">
<TextBox.InnerRightContent>
<Button
Classes="transparent-full"
Command="{Binding ClearSearchQuery}"
IsVisible="{Binding SearchQuery.Length}">
<ui:SymbolIcon Symbol="Cancel" />
</Button>
</TextBox.InnerRightContent>
</TextBox>
<Button
Grid.Column="1"
Width="80"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchModelsCommand}"
IsDefault="True">
<Grid>
<controls:ProgressRing
MinWidth="16"
MinHeight="16"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding SearchModelsCommand.IsRunning}" />
<TextBlock
VerticalAlignment="Center"
IsVisible="{Binding !SearchModelsCommand.IsRunning}"
Text="{x:Static lang:Resources.Action_Search}" />
</Grid>
</Button>
</Grid>
<DockPanel>
<StackPanel Margin="0,8,4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllSortModes}"
SelectedItem="{Binding SortMode}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllCivitPeriods}"
SelectedItem="{Binding SelectedPeriod}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_ModelType}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllModelTypes}"
SelectedItem="{Binding SelectedModelType}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_BaseModel}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding BaseModelOptions}"
SelectedItem="{Binding SelectedBaseModelType}" />
</StackPanel>
<CheckBox
Margin="8,8,0,0"
HorizontalAlignment="Right"
Content="{x:Static lang:Resources.Label_ShowNsfwContent}"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" />
</DockPanel>
</StackPanel>
<ScrollViewer
Grid.Row="1"
ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}"
HorizontalAlignment="Center"
ItemsSource="{Binding ModelCards}">
<ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<TextBlock
Grid.Row="2"
Margin="8,8"
VerticalAlignment="Bottom"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" />
<StackPanel
Grid.Row="2"
Margin="8"
HorizontalAlignment="Center"
IsVisible="{Binding HasSearched}"
Orientation="Vertical">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Margin="0,0,0,8">
<TextBlock Margin="0,0,4,0" TextAlignment="Center"
Text="{x:Static lang:Resources.Label_Page}"
VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding CurrentPageNumber, FallbackValue=1}"
VerticalAlignment="Center"
SpinButtonPlacementMode="Hidden"
TextAlignment="Center"/>
<TextBlock Margin="4,0,0,0" VerticalAlignment="Center">
<Run Text="/"/>
<Run Text="{Binding TotalPages, FallbackValue=5}"/>
</TextBlock>
</StackPanel>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,8,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<Button
Margin="0,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
</StackPanel>
<TextBlock
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
IsVisible="{Binding NoResultsFound}"
Text="{Binding NoResultsText, FallbackValue=No results found}" />
<controls:ProgressRing
Grid.Row="1"
Width="128"
Height="128"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsIndeterminate="True"
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" />
</Grid>
</controls:UserControlBase>

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

@ -0,0 +1,41 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Core.Attributes;
using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel;
namespace StabilityMatrix.Avalonia.Views;
[Singleton]
public partial class CivitAiBrowserPage : UserControlBase
{
public CivitAiBrowserPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (sender is not ScrollViewer scrollViewer)
return;
var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum;
Debug.WriteLine($"IsAtEnd: {isAtEnd}");
}
private void InputElement_OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape && DataContext is CivitAiBrowserViewModel viewModel)
{
viewModel.ClearSearchQuery();
}
}
}

37
StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml

@ -8,45 +8,35 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:huggingFacePage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.HuggingFacePage"
xmlns:huggingFace="clr-namespace:StabilityMatrix.Avalonia.Models.HuggingFace"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
d:DataContext="{x:Static mocks:DesignData.HuggingFacePageViewModel}"
d:DesignHeight="650"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="vm:HuggingFacePageViewModel"
x:DataType="checkpointBrowser:HuggingFacePageViewModel"
Focusable="True"
mc:Ignorable="d">
<ScrollViewer>
<Grid RowDefinitions="Auto,Auto,Auto,Auto"
ColumnDefinitions="*, Auto"
ColumnDefinitions="Auto, *"
Margin="8"
HorizontalAlignment="Stretch">
<Grid ColumnDefinitions="Auto, *"
HorizontalAlignment="Stretch">
<TextBlock
Text="Hugging Face Model Import"
FontSize="22"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
HorizontalAlignment="Right"
IsVisible="{Binding !!NumSelected}"
Margin="8,0,0,0">
<Run Text="{Binding NumSelected}" />
<Run Text="models selected" />
</TextBlock>
</Grid>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
IsVisible="{Binding !!NumSelected}"
Margin="8,0,8,0">
<Run Text="{Binding NumSelected}" />
<Run Text="models selected" />
</TextBlock>
<ui:CommandBar
Grid.Row="0"
Grid.Column="1"
Margin="8,-1,0,0"
Grid.Column="0"
Margin="0,-1,0,0"
VerticalAlignment="Center"
HorizontalAlignment="Right"
HorizontalAlignment="Left"
VerticalContentAlignment="Center"
DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands>
@ -66,6 +56,7 @@
Command="{Binding ClearSelection}"
IsEnabled="{Binding !!NumSelected}"
Label="{x:Static lang:Resources.Action_ClearSelection}" />
<ui:CommandBarSeparator IsVisible="{Binding !!NumSelected}" />
</ui:CommandBar.PrimaryCommands>
</ui:CommandBar>

Loading…
Cancel
Save