JT
12 months ago
10 changed files with 1354 additions and 1355 deletions
@ -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; } |
||||
} |
@ -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"; |
||||
} |
@ -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> |
||||
|
@ -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> |
@ -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(); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue