Browse Source

Added selectable image button control for Outputs page & beyond

pull/240/head
JT 1 year ago
parent
commit
d3d10be753
  1. 1
      StabilityMatrix.Avalonia/App.axaml
  2. 51
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  3. 25
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  4. 26
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  5. 18
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  6. 8
      StabilityMatrix.Avalonia/Languages/Resources.resx
  7. 19
      StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs
  8. 74
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  9. 78
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml

1
StabilityMatrix.Avalonia/App.axaml

@ -60,6 +60,7 @@
<StyleInclude Source="Controls/SharpenCard.axaml"/>
<StyleInclude Source="Controls/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Paginator.axaml"/>
<StyleInclude Source="Controls/SelectableImageCard/SelectableImageButton.axaml"/>
<Style Selector="DockControl">
<Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />

51
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml

@ -0,0 +1,51 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
<selectableImageCard:SelectableImageButton
Source="https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg" />
</Border>
</Design.PreviewWith>
<!-- Add Styles Here -->
<Style Selector="selectableImageCard|SelectableImageButton">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<CheckBox VerticalAlignment="Top"
HorizontalAlignment="Right"
Margin="14,8"
Padding="0"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ZIndex="100">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
</CheckBox.RenderTransform>
<CheckBox.Styles>
<Style Selector="CheckBox">
<Setter Property="CornerRadius" Value="16" />
</Style>
</CheckBox.Styles>
</CheckBox>
<Button
Margin="0"
Padding="4"
CornerRadius="12"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}">
<controls:BetterAdvancedImage
Width="300"
Height="300"
Stretch="UniformToFill"
CornerRadius="8"
ContextFlyout="{TemplateBinding ContextFlyout}"
Source="{TemplateBinding Source}" />
</Button>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

25
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs

@ -0,0 +1,25 @@
using Avalonia;
using Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Controls.SelectableImageCard;
public class SelectableImageButton : Button
{
public static readonly StyledProperty<bool?> IsSelectedProperty =
CheckBox.IsCheckedProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<string?> SourceProperty =
BetterAdvancedImage.SourceProperty.AddOwner<SelectableImageButton>();
public bool? IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
public string? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
}

26
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -7,6 +7,7 @@ using System.IO;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
@ -20,6 +21,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
@ -29,6 +31,7 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
@ -336,8 +339,27 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static OutputsPageViewModel OutputsPageViewModel =>
Services.GetRequiredService<OutputsPageViewModel>();
public static OutputsPageViewModel OutputsPageViewModel
{
get
{
var vm = Services.GetRequiredService<OutputsPageViewModel>();
vm.Outputs = new ObservableCollectionExtended<OutputImageViewModel>
{
new(
new LocalImageFile
{
AbsolutePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ImageType = LocalImageFileType.TextToImage
}
)
};
return vm;
}
}
public static PackageManagerViewModel PackageManagerViewModel
{

18
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -257,6 +257,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Open in Image Viewer.
/// </summary>
public static string Action_OpenInViewer {
get {
return ResourceManager.GetString("Action_OpenInViewer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open on CivitAI.
/// </summary>
@ -1157,6 +1166,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to {0} images selected.
/// </summary>
public static string Label_NumImagesSelected {
get {
return ResourceManager.GetString("Label_NumImagesSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only available on Windows.
/// </summary>

8
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -687,4 +687,10 @@
<data name="Action_Copy" xml:space="preserve">
<value>Copy</value>
</data>
</root>
<data name="Action_OpenInViewer" xml:space="preserve">
<value>Open in Image Viewer</value>
</data>
<data name="Label_NumImagesSelected" xml:space="preserve">
<value>{0} images selected</value>
</data>
</root>

19
StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs

@ -0,0 +1,19 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Models.Database;
namespace StabilityMatrix.Avalonia.ViewModels.OutputsPage;
public partial class OutputImageViewModel : ViewModelBase
{
public LocalImageFile ImageFile { get; }
[ObservableProperty]
private bool isSelected;
public OutputImageViewModel(LocalImageFile imageFile)
{
ImageFile = imageFile;
}
}

74
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -4,21 +4,24 @@ using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
@ -32,21 +35,23 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(OutputsPage))]
[View(typeof(Views.OutputsPage))]
public partial class OutputsPageViewModel : PageViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
private readonly ILogger<OutputsPageViewModel> logger;
public override string Title => "Outputs";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
public SourceCache<LocalImageFile, string> OutputsCache { get; } = new(p => p.AbsolutePath);
public SourceCache<OutputImageViewModel, string> OutputsCache { get; } =
new(p => p.ImageFile.AbsolutePath);
public IObservableCollection<LocalImageFile> Outputs { get; } =
new ObservableCollectionExtended<LocalImageFile>();
public IObservableCollection<OutputImageViewModel> Outputs { get; set; } =
new ObservableCollectionExtended<OutputImageViewModel>();
public IEnumerable<SharedOutputType> OutputTypes { get; } = Enum.GetValues<SharedOutputType>();
@ -60,25 +65,35 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty]
private SharedOutputType selectedOutputType;
[ObservableProperty]
private int numItemsSelected;
public bool CanShowOutputTypes => SelectedCategory.Name.Equals("Shared Output Folder");
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
INotificationService notificationService,
INavigationService navigationService
INavigationService navigationService,
ILogger<OutputsPageViewModel> logger
)
{
this.settingsManager = settingsManager;
this.notificationService = notificationService;
this.navigationService = navigationService;
this.logger = logger;
OutputsCache
.Connect()
.DeferUntilLoaded()
.SortBy(x => x.CreatedAt, SortDirection.Descending)
.SortBy(x => x.ImageFile.CreatedAt, SortDirection.Descending)
.ObserveOn(SynchronizationContext.Current)
.Bind(Outputs)
.Subscribe();
.WhenPropertyChanged(p => p.IsSelected)
.Subscribe(_ =>
{
NumItemsSelected = Outputs.Count(o => o.IsSelected);
});
if (!settingsManager.IsLibraryDirSet || Design.IsDesignMode)
return;
@ -152,16 +167,29 @@ public partial class OutputsPageViewModel : PageViewModelBase
GetOutputs(path);
}
public async Task OnImageClick(LocalImageFile item)
public async Task OnImageClick(OutputImageViewModel item)
{
// Select image if we're in "select mode"
if (NumItemsSelected > 0)
{
item.IsSelected = !item.IsSelected;
}
else
{
await ShowImageDialog(item);
}
}
public async Task ShowImageDialog(OutputImageViewModel item)
{
var currentIndex = Outputs.IndexOf(item);
var image = new ImageSource(new FilePath(item.AbsolutePath));
var image = new ImageSource(new FilePath(item.ImageFile.AbsolutePath));
// Preload
await image.GetBitmapAsync();
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item };
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile };
using var onNext = Observable
.FromEventPattern<DirectionalNavigationEventArgs>(
@ -180,14 +208,14 @@ public partial class OutputsPageViewModel : PageViewModelBase
{
var newImage = Outputs[newIndex];
var newImageSource = new ImageSource(
new FilePath(newImage.AbsolutePath)
new FilePath(newImage.ImageFile.AbsolutePath)
);
// Preload
await newImageSource.GetBitmapAsync();
sender.ImageSource = newImageSource;
sender.LocalImageFile = newImage;
sender.LocalImageFile = newImage.ImageFile;
currentIndex = newIndex;
}
@ -207,9 +235,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
public async Task OpenImage(string imagePath) => await ProcessRunner.OpenFileBrowser(imagePath);
public async Task DeleteImage(LocalImageFile? item)
public async Task DeleteImage(OutputImageViewModel? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
if (item?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
return;
}
@ -223,7 +251,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
return;
}
Outputs.Remove(item);
OutputsCache.Remove(item);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
@ -244,6 +272,14 @@ public partial class OutputsPageViewModel : PageViewModelBase
EventManager.Instance.OnInferenceUpscaleRequested(image);
}
public void ClearSelection()
{
foreach (var output in Outputs)
{
output.IsSelected = false;
}
}
private void GetOutputs(string directory)
{
if (!settingsManager.IsLibraryDirSet)
@ -257,9 +293,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
var list = Directory
.EnumerateFiles(directory, "*.png", SearchOption.AllDirectories)
.Select(file => LocalImageFile.FromPath(file))
.OrderByDescending(f => f.CreatedAt);
.Select(file => new OutputImageViewModel(LocalImageFile.FromPath(file)))
.OrderByDescending(f => f.ImageFile.CreatedAt);
OutputsCache.EditDiff(list, (x, y) => x.AbsolutePath == y.AbsolutePath);
OutputsCache.EditDiff(list, (x, y) => x.ImageFile.AbsolutePath == y.ImageFile.AbsolutePath);
}
}

78
StabilityMatrix.Avalonia/Views/OutputsPage.axaml

@ -13,7 +13,8 @@
xmlns:models1="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}"
d:DesignHeight="450"
d:DesignWidth="700"
@ -24,8 +25,9 @@
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,16"
HorizontalAlignment="Left">
<Grid RowDefinitions="Auto, *">
<TextBlock Text="Output Folder" Margin="4"/>
<TextBlock Text="Output Folder" Margin="4" />
<ComboBox Grid.Row="1" ItemsSource="{Binding Categories}"
CornerRadius="8"
SelectedItem="{Binding SelectedCategory}"
MinWidth="150">
<ComboBox.Styles>
@ -48,13 +50,31 @@
</Grid>
<Grid RowDefinitions="Auto, *" Margin="8,0"
IsVisible="{Binding CanShowOutputTypes}">
<TextBlock Text="Output Type" Margin="4"/>
<TextBlock Text="Output Type" Margin="4" />
<ComboBox Grid.Row="1" ItemsSource="{Binding OutputTypes}"
CornerRadius="8"
SelectedItem="{Binding SelectedOutputType}"
MinWidth="150"
VerticalAlignment="Stretch"
VerticalContentAlignment="Center"/>
VerticalContentAlignment="Center" />
</Grid>
<TextBlock IsVisible="{Binding !!NumItemsSelected}"
FontSize="16"
Margin="8, 0"
VerticalAlignment="Center"
TextAlignment="Center">
<Run Text="{Binding NumItemsSelected}" />
<Run Text="images selected" />
</TextBlock>
<Button Content="Clear Selection"
VerticalAlignment="Bottom"
CornerRadius="8"
Margin="8, 0"
Height="40"
Command="{Binding ClearSelection}"
IsVisible="{Binding !!NumItemsSelected}" />
</StackPanel>
<ScrollViewer Grid.Row="1">
@ -65,45 +85,45 @@
<UniformGridLayout MinColumnSpacing="16" MinRowSpacing="16" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type database:LocalImageFile}">
<Button
Margin="0"
Padding="4"
<DataTemplate DataType="{x:Type outputsPage:OutputImageViewModel}">
<selectableImageCard:SelectableImageButton
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).OnImageClick}"
CommandParameter="{Binding }">
<controls:BetterAdvancedImage
Width="300"
Height="300"
Stretch="UniformToFill"
Source="{Binding AbsolutePath}" />
<Button.ContextFlyout>
CommandParameter="{Binding }"
IsSelected="{Binding IsSelected}"
Source="{Binding ImageFile.AbsolutePath}">
<selectableImageCard:SelectableImageButton.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_Delete}"
IconSource="Delete"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).DeleteImage}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_Copy}"
IconSource="Copy"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).CopyImage}"
CommandParameter="{Binding AbsolutePath}" />
CommandParameter="{Binding ImageFile.AbsolutePath}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_OpenInExplorer}"
IconSource="Folder"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).OpenImage}"
CommandParameter="{Binding AbsolutePath}" />
CommandParameter="{Binding ImageFile.AbsolutePath}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_OpenInViewer}"
IconSource="Image"
IsVisible="{Binding !!$parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).NumItemsSelected}"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).ShowImageDialog}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_Delete}"
IconSource="Delete"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).DeleteImage}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutSeparator
IsVisible="{Binding GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}" />
IsVisible="{Binding ImageFile.GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ui:MenuFlyoutSubItem Text="Send to Inference" IconSource="Share"
IsVisible="{Binding GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}">
IsVisible="{Binding ImageFile.GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}">
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="Text to Image"
@ -135,8 +155,8 @@
</ui:MenuFlyoutItem>
</ui:MenuFlyoutSubItem>
</ui:FAMenuFlyout>
</Button.ContextFlyout>
</Button>
</selectableImageCard:SelectableImageButton.ContextFlyout>
</selectableImageCard:SelectableImageButton>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>

Loading…
Cancel
Save