Browse Source

added basic card UI, searching, and infiniscroll

pull/629/head
JT 9 months ago
parent
commit
c26960ce1f
  1. 2
      Avalonia.Gif/Avalonia.Gif.csproj
  2. 4
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  3. 16
      StabilityMatrix.Avalonia/App.axaml.cs
  4. 8
      StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs
  5. 15
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  6. 175
      StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs
  7. 340
      StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml
  8. 32
      StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs
  9. 2
      StabilityMatrix.Core/Api/IOpenArtApi.cs
  10. 2
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs
  11. 14
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs
  12. 3
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs
  13. 2
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs
  14. 2
      StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs
  15. 13
      StabilityMatrix.Core/Models/Packages/SDWebForge.cs
  16. 2
      StabilityMatrix.UITests/StabilityMatrix.UITests.csproj

2
Avalonia.Gif/Avalonia.Gif.csproj

@ -10,7 +10,7 @@
<EnableWindowsTargeting>true</EnableWindowsTargeting> <EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.7" /> <PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="SkiaSharp" Version="2.88.7" /> <PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" /> <PackageReference Include="DotNet.Bundle" Version="0.9.13" />
</ItemGroup> </ItemGroup>

4
StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj

@ -19,8 +19,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.7" /> <PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.7" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" /> <PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />

16
StabilityMatrix.Avalonia/App.axaml.cs

@ -333,7 +333,8 @@ public sealed class App : Application
provider.GetRequiredService<NewPackageManagerViewModel>(), provider.GetRequiredService<NewPackageManagerViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(), provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(), provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>() provider.GetRequiredService<OutputsPageViewModel>(),
provider.GetRequiredService<OpenArtBrowserViewModel>()
}, },
FooterPages = { provider.GetRequiredService<SettingsViewModel>() } FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
} }
@ -556,7 +557,7 @@ public sealed class App : Application
.ConfigureHttpClient(c => .ConfigureHttpClient(c =>
{ {
c.BaseAddress = new Uri("https://civitai.com"); c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(15); c.Timeout = TimeSpan.FromSeconds(30);
}) })
.AddPolicyHandler(retryPolicy); .AddPolicyHandler(retryPolicy);
@ -565,7 +566,7 @@ public sealed class App : Application
.ConfigureHttpClient(c => .ConfigureHttpClient(c =>
{ {
c.BaseAddress = new Uri("https://civitai.com"); c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(15); c.Timeout = TimeSpan.FromSeconds(30);
}) })
.AddPolicyHandler(retryPolicy); .AddPolicyHandler(retryPolicy);
@ -583,6 +584,15 @@ public sealed class App : Application
new TokenAuthHeaderHandler(serviceProvider.GetRequiredService<LykosAuthTokenProvider>()) new TokenAuthHeaderHandler(serviceProvider.GetRequiredService<LykosAuthTokenProvider>())
); );
services
.AddRefitClient<IOpenArtApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://openart.ai/api/public/workflows");
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(retryPolicy);
// Add Refit client managers // Add Refit client managers
services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));

8
StabilityMatrix.Avalonia/Models/IInfinitelyScroll.cs

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Models;
public interface IInfinitelyScroll
{
Task LoadNextPageAsync();
}

15
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -39,13 +39,14 @@
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.1" /> <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.1" />
<PackageReference Include="AutoComplete.Net" Version="1.2211.2014.42"/> <PackageReference Include="AutoComplete.Net" Version="1.2211.2014.42"/>
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" /> <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.7" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0.2" /> <PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0.2" />
<PackageReference Include="Avalonia" Version="11.0.7" /> <PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.7" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.9" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.7" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.9" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.7" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.9" />
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" /> <PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Labs.Controls" Version="11.0.3" /> <PackageReference Include="Avalonia.Labs.Controls" Version="11.0.3" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.6" /> <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.6" />
@ -65,8 +66,8 @@
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" /> <PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
<PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" /> <PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.5" /> <PackageReference Include="FluentAvaloniaUI" Version="2.0.5" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.225" /> <PackageReference Include="FluentIcons.Avalonia" Version="1.1.227" />
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="1.1.225" /> <PackageReference Include="FluentIcons.Avalonia.Fluent" Version="1.1.227" />
<PackageReference Include="FuzzySharp" Version="2.0.2" /> <PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.2" /> <PackageReference Include="Markdown.Avalonia" Version="11.0.2" />

175
StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs

@ -0,0 +1,175 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using Refit;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.OpenArt;
using StabilityMatrix.Core.Processes;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(OpenArtBrowserPage))]
[Singleton]
public partial class OpenArtBrowserViewModel(IOpenArtApi openArtApi, INotificationService notificationService)
: PageViewModelBase,
IInfinitelyScroll
{
private const int PageSize = 20;
public override string Title => "Workflows";
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Whiteboard };
private SourceCache<OpenArtSearchResult, string> searchResultsCache = new(x => x.Id);
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PageCount), nameof(CanGoBack), nameof(CanGoForward), nameof(CanGoToEnd))]
private OpenArtSearchResponse? latestSearchResponse;
[ObservableProperty]
private IObservableCollection<OpenArtSearchResult> searchResults =
new ObservableCollectionExtended<OpenArtSearchResult>();
[ObservableProperty]
private string searchQuery = string.Empty;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(InternalPageNumber), nameof(CanGoBack))]
private int displayedPageNumber = 1;
public int InternalPageNumber => DisplayedPageNumber - 1;
public int PageCount =>
Math.Max(
1,
Convert.ToInt32(Math.Ceiling((LatestSearchResponse?.Total ?? 0) / Convert.ToDouble(PageSize)))
);
public bool CanGoBack => InternalPageNumber > 0;
public bool CanGoForward => PageCount > InternalPageNumber + 1;
public bool CanGoToEnd => PageCount > InternalPageNumber + 1;
protected override void OnInitialLoaded()
{
searchResultsCache.Connect().DeferUntilLoaded().Bind(SearchResults).Subscribe();
}
public override async Task OnLoadedAsync()
{
if (SearchResults.Any())
return;
await DoSearch();
}
[RelayCommand]
private async Task FirstPage()
{
DisplayedPageNumber = 1;
await DoSearch();
}
[RelayCommand]
private async Task PreviousPage()
{
DisplayedPageNumber--;
await DoSearch(InternalPageNumber);
}
[RelayCommand]
private async Task NextPage()
{
DisplayedPageNumber++;
await DoSearch(InternalPageNumber);
}
[RelayCommand]
private async Task LastPage()
{
DisplayedPageNumber = PageCount;
await DoSearch(PageCount - 1);
}
[Localizable(false)]
[RelayCommand]
private void OpenModel(OpenArtSearchResult workflow)
{
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}");
}
[RelayCommand]
private async Task SearchButton()
{
DisplayedPageNumber = 1;
await DoSearch();
}
private async Task DoSearch(int page = 0)
{
IsLoading = true;
try
{
var response = await openArtApi.SearchAsync(
new OpenArtSearchRequest
{
Keyword = SearchQuery,
PageSize = PageSize,
CurrentPage = page
}
);
searchResultsCache.EditDiff(response.Items, (a, b) => a.Id == b.Id);
LatestSearchResponse = response;
}
catch (ApiException e)
{
notificationService.Show("Error retrieving workflows", e.Message);
}
finally
{
IsLoading = false;
}
}
public async Task LoadNextPageAsync()
{
try
{
DisplayedPageNumber++;
var response = await openArtApi.SearchAsync(
new OpenArtSearchRequest
{
Keyword = SearchQuery,
PageSize = PageSize,
CurrentPage = InternalPageNumber
}
);
searchResultsCache.AddOrUpdate(response.Items);
LatestSearchResponse = response;
}
catch (ApiException e)
{
notificationService.Show("Unable to load the next page", e.Message);
}
}
}

340
StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml

@ -0,0 +1,340 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:openArt="clr-namespace:StabilityMatrix.Core.Models.Api.OpenArt;assembly=StabilityMatrix.Core"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
x:DataType="viewModels:OpenArtBrowserViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StabilityMatrix.Avalonia.Views.OpenArtBrowserPage">
<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>
<input:StandardUICommand
x:Key="OpenModelCommand"
Command="{Binding OpenModelCommand}" />
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter" />
<DataTemplate x:Key="OpenArtWorkflowTemplate" DataType="{x:Type openArt:OpenArtSearchResult}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}">
<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.Row="0"
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding Thumbnails[0].Url}"
Stretch="UniformToFill"
StretchDirection="Both" />
<!-- 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 Creator.DevProfileUrl}"
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 Creator.Avatar, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding Creator.Avatar}" />
<TextBlock
VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding Creator.Name}" />
</StackPanel>
</Button>
</Border>
<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 Name}"
TextWrapping="NoWrap"
ToolTip.Tip="{Binding Name}" />
<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 Stats.Rating}" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Text="{Binding Stats.NumReviews}"
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 Stats.NumLikes, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
Margin="0,0,4,0"
VerticalAlignment="Center"
Text="{Binding Stats.NumDownloads, 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="{StaticResource OpenModelCommand}"
CommandParameter="{Binding }"
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, *, Auto">
<TextBlock Text="OpenArt Workflow Browser"
FontSize="24"
Margin="8,8" />
<Grid Grid.Row="1" ColumnDefinitions="*,Auto"
Margin="8,0">
<TextBox
HorizontalAlignment="Stretch"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="{x:Static lang:Resources.Action_Search}"
Classes="search"/>
<Button
Grid.Column="1"
Width="80"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchButtonCommand}"
IsDefault="True">
<Grid>
<controls:ProgressRing
MinWidth="16"
MinHeight="16"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding SearchButtonCommand.IsRunning}" />
<TextBlock
VerticalAlignment="Center"
IsVisible="{Binding !SearchButtonCommand.IsRunning}"
Text="{x:Static lang:Resources.Action_Search}" />
</Grid>
</Button>
</Grid>
<controls:ProgressRing Grid.Row="2"
IsVisible="{Binding IsLoading}"
IsIndeterminate="True"
Width="128"
Height="128"/>
<ScrollViewer Grid.Row="2"
ScrollChanged="ScrollViewer_OnScrollChanged"
IsVisible="{Binding !IsLoading}">
<ItemsRepeater ItemsSource="{Binding SearchResults}"
ItemTemplate="{StaticResource OpenArtWorkflowTemplate}">
<ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="4" MinRowSpacing="4"/>
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<StackPanel Grid.Row="3"
HorizontalAlignment="Center"
Margin="0,8,0,8"
Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPageCommand}"
IsEnabled="{Binding CanGoBack}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,16,0"
Command="{Binding PreviousPageCommand}"
IsEnabled="{Binding CanGoBack}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<TextBlock Margin="8,0,4,0" TextAlignment="Center"
Text="{x:Static lang:Resources.Label_Page}"
VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding DisplayedPageNumber, FallbackValue=1}"
VerticalAlignment="Center"
SpinButtonPlacementMode="Hidden"
TextAlignment="Center"/>
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center">
<Run Text="/"/>
<Run Text="{Binding PageCount, FallbackValue=5}"/>
</TextBlock>
<Button
Margin="16,0,8,0"
Command="{Binding NextPageCommand}"
IsEnabled="{Binding CanGoForward}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPageCommand}"
IsEnabled="{Binding CanGoToEnd}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
</Grid>
</controls:UserControlBase>

32
StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs

@ -0,0 +1,32 @@
using System;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views;
[Singleton]
public partial class OpenArtBrowserPage : UserControlBase
{
public OpenArtBrowserPage()
{
InitializeComponent();
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (sender is not ScrollViewer scrollViewer)
return;
if (scrollViewer.Offset.Y == 0)
return;
var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 0.1f;
if (isAtEnd && DataContext is IInfinitelyScroll scroll)
{
scroll.LoadNextPageAsync().SafeFireAndForget();
}
}
}

2
StabilityMatrix.Core/Api/IOpenArtApi.cs

@ -10,7 +10,7 @@ public interface IOpenArtApi
Task<OpenArtSearchResponse> GetFeedAsync([Query] OpenArtFeedRequest request); Task<OpenArtSearchResponse> GetFeedAsync([Query] OpenArtFeedRequest request);
[Get("/list")] [Get("/list")]
Task<OpenArtSearchResponse> SearchAsync([Query] OpenArtFeedRequest request); Task<OpenArtSearchResponse> SearchAsync([Query] OpenArtSearchRequest request);
[Post("/download")] [Post("/download")]
Task<OpenArtDownloadResponse> DownloadWorkflowAsync([Body] OpenArtDownloadRequest request); Task<OpenArtDownloadResponse> DownloadWorkflowAsync([Body] OpenArtDownloadRequest request);

2
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs

@ -20,5 +20,5 @@ public class OpenArtCreator
public string Username { get; set; } public string Username { get; set; }
[JsonPropertyName("dev_profile_url")] [JsonPropertyName("dev_profile_url")]
public Uri DevProfileUrl { get; set; } public string DevProfileUrl { get; set; }
} }

14
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Api.OpenArt;
public class OpenArtDateTime
{
[JsonPropertyName("_seconds")]
public long Seconds { get; set; }
public DateTimeOffset ToDateTimeOffset()
{
return DateTimeOffset.FromUnixTimeSeconds(Seconds);
}
}

3
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs

@ -2,6 +2,9 @@
namespace StabilityMatrix.Core.Models.Api.OpenArt; namespace StabilityMatrix.Core.Models.Api.OpenArt;
/// <summary>
/// Note that parameters Category, Custom Node and Sort should be used separately
/// </summary>
public class OpenArtFeedRequest public class OpenArtFeedRequest
{ {
[AliasAs("category")] [AliasAs("category")]

2
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs

@ -32,7 +32,7 @@ public class OpenArtSearchResult
public IEnumerable<string> Categories { get; set; } public IEnumerable<string> Categories { get; set; }
[JsonPropertyName("thumbnails")] [JsonPropertyName("thumbnails")]
public IEnumerable<OpenArtThumbnail> Thumbnails { get; set; } public List<OpenArtThumbnail> Thumbnails { get; set; }
[JsonPropertyName("nodes_count")] [JsonPropertyName("nodes_count")]
public NodesCount NodesCount { get; set; } public NodesCount NodesCount { get; set; }

2
StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs

@ -14,7 +14,7 @@ public class OpenArtStats
public int NumReviews { get; set; } public int NumReviews { get; set; }
[JsonPropertyName("rating")] [JsonPropertyName("rating")]
public int Rating { get; set; } public double Rating { get; set; }
[JsonPropertyName("num_comments")] [JsonPropertyName("num_comments")]
public int NumComments { get; set; } public int NumComments { get; set; }

13
StabilityMatrix.Core/Models/Packages/SDWebForge.cs

@ -79,6 +79,19 @@ public class SDWebForge(
TorchVersion.Mps TorchVersion.Mps
}; };
public override Dictionary<SharedOutputType, IReadOnlyList<string>> SharedOutputFolders =>
new()
{
[SharedOutputType.Extras] = new[] { "output/extras-images" },
[SharedOutputType.Saved] = new[] { "log/images" },
[SharedOutputType.Img2Img] = new[] { "output/img2img-images" },
[SharedOutputType.Text2Img] = new[] { "output/txt2img-images" },
[SharedOutputType.Img2ImgGrids] = new[] { "output/img2img-grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "output/txt2img-grids" }
};
public override string OutputFolderName => "output";
public override async Task InstallPackage( public override async Task InstallPackage(
string installLocation, string installLocation,
TorchVersion torchVersion, TorchVersion torchVersion,

2
StabilityMatrix.UITests/StabilityMatrix.UITests.csproj

@ -15,7 +15,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Headless.XUnit" Version="11.0.7" /> <PackageReference Include="Avalonia.Headless.XUnit" Version="11.0.9" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" /> <PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />

Loading…
Cancel
Save