JT
9 months ago
16 changed files with 614 additions and 18 deletions
@ -0,0 +1,8 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Models; |
||||||
|
|
||||||
|
public interface IInfinitelyScroll |
||||||
|
{ |
||||||
|
Task LoadNextPageAsync(); |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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> |
@ -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(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue