Browse Source

Add dragging images to load project state

pull/165/head
Ionite 1 year ago
parent
commit
99425d43c9
No known key found for this signature in database
  1. 13
      StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs
  2. 31
      StabilityMatrix.Avalonia/Controls/DropTargetUserControlBase.cs
  3. 10
      StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml
  4. 93
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  5. 26
      StabilityMatrix.Avalonia/ViewModels/Inference/IImageGalleryComponent.cs
  6. 157
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  7. 37
      StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs
  8. 28
      StabilityMatrix.Core/Models/Database/LocalImageFile.cs

13
StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs

@ -12,7 +12,7 @@ namespace StabilityMatrix.Avalonia.Controls.Dock;
/// Base for Dock controls
/// Expects a <see cref="DockControl"/> named "Dock" in the XAML
/// </summary>
public abstract class DockUserControlBase : UserControlBase
public abstract class DockUserControlBase : DropTargetUserControlBase
{
private DockControl _dock = null!;
protected readonly AvaloniaDockSerializer DockSerializer = new();
@ -22,10 +22,11 @@ public abstract class DockUserControlBase : UserControlBase
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_dock = this.FindControl<DockControl>("Dock")
?? throw new NullReferenceException("DockControl not found");
_dock =
this.FindControl<DockControl>("Dock")
?? throw new NullReferenceException("DockControl not found");
if (_dock.Layout is { } layout)
{
DockState.Save(layout);
@ -40,7 +41,7 @@ public abstract class DockUserControlBase : UserControlBase
DockState.Restore(layout);
}
}
protected virtual string SaveDockLayout()
{
return DockSerializer.Serialize(_dock.Layout);

31
StabilityMatrix.Avalonia/Controls/DropTargetUserControlBase.cs

@ -0,0 +1,31 @@
using Avalonia.Input;
using StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.Controls;
public abstract class DropTargetUserControlBase : UserControlBase
{
protected DropTargetUserControlBase()
{
AddHandler(DragDrop.DropEvent, DropHandler);
AddHandler(DragDrop.DragOverEvent, DragOverHandler);
DragDrop.SetAllowDrop(this, true);
}
private void DragOverHandler(object? sender, DragEventArgs e)
{
if (DataContext is IDropTarget dropTarget)
{
dropTarget.DragOver(sender, e);
}
}
private void DropHandler(object? sender, DragEventArgs e)
{
if (DataContext is IDropTarget dropTarget)
{
dropTarget.Drop(sender, e);
}
}
}

10
StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml

@ -39,11 +39,11 @@
HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalAlignment}">
<controls:Card.Styles>
<!--<controls:Card.Styles>
<Style Selector="ItemsRepeater">
<Setter Property="animations:ItemsRepeaterArrangeAnimation.EnableItemsArrangeAnimation" Value="True"/>
</Style>
</controls:Card.Styles>
</controls:Card.Styles>-->
<Grid RowDefinitions="Auto,*">
<TextBox
@ -128,6 +128,12 @@
Classes="transparent-full"
CornerRadius="8">
<Interaction.Behaviors>
<BehaviorCollection>
<ContextDragBehavior HorizontalDragThreshold="3" VerticalDragThreshold="3"/>
</BehaviorCollection>
</Interaction.Behaviors>
<Button.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem

93
StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs

@ -1,28 +1,38 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase, IDisposable, IPersistentViewProvider
public abstract partial class InferenceTabViewModelBase
: LoadableViewModelBase,
IDisposable,
IPersistentViewProvider,
IDropTarget
{
/// <summary>
/// The title of the tab
/// </summary>
public virtual string TabTitle => ProjectFile?.NameWithoutExtension ?? "New Project";
/// <summary>
/// Whether there are unsaved changes
/// </summary>
[ObservableProperty]
[property: JsonIgnore]
private bool hasUnsavedChanges;
/// <summary>
/// The tab's project file
/// </summary>
@ -30,22 +40,91 @@ public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase,
[NotifyPropertyChangedFor(nameof(TabTitle))]
[property: JsonIgnore]
private FilePath? projectFile;
/// <inheritdoc />
Control? IPersistentViewProvider.AttachedPersistentView { get; set; }
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
((IPersistentViewProvider) this).AttachedPersistentView = null;
((IPersistentViewProvider)this).AttachedPersistentView = null;
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void DragOver(object? sender, DragEventArgs e)
{
// 1. Context drop for LocalImageFile
if (e.Data.GetDataFormats().Contains("Context"))
{
if (e.Data.Get("Context") is LocalImageFile imageFile)
{
e.Handled = true;
return;
}
e.DragEffects = DragDropEffects.None;
}
// 2. OS Files
if (e.Data.GetDataFormats().Contains(DataFormats.Files))
{
e.Handled = true;
return;
}
e.DragEffects = DragDropEffects.None;
}
/// <inheritdoc />
public void Drop(object? sender, DragEventArgs e)
{
// 1. Context drop for LocalImageFile
if (e.Data.GetDataFormats().Contains("Context"))
{
if (e.Data.Get("Context") is LocalImageFile imageFile)
{
e.Handled = true;
Dispatcher.UIThread.Post(() =>
{
var metadata = imageFile.ReadMetadata();
if (metadata.SMProject is not null)
{
var project = JsonSerializer.Deserialize<InferenceProjectDocument>(
metadata.SMProject
);
// Check project type matches
if (project?.GetViewModelType() == GetType() && project.State is not null)
{
LoadStateFromJsonObject(project.State);
}
// Load image
if (this is IImageGalleryComponent imageGalleryComponent)
{
imageGalleryComponent.LoadImagesToGallery(
new ImageSource(imageFile.GlobalFullPath)
);
}
}
});
return;
}
}
// 2. OS Files
if (e.Data.GetDataFormats().Contains(DataFormats.Files))
{
e.Handled = true;
}
}
}

26
StabilityMatrix.Avalonia/ViewModels/Inference/IImageGalleryComponent.cs

@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using StabilityMatrix.Avalonia.Models;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
public interface IImageGalleryComponent
{
ImageGalleryCardViewModel ImageGalleryCardViewModel { get; }
/// <summary>
/// Clears existing images and loads new ones
/// </summary>
public void LoadImagesToGallery(params ImageSource[] imageSources)
{
ImageGalleryCardViewModel.ImageSources.Clear();
foreach (var imageSource in imageSources)
{
ImageGalleryCardViewModel.ImageSources.Add(imageSource);
}
ImageGalleryCardViewModel.SelectedImage = imageSources.FirstOrDefault();
}
}

157
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs

@ -38,7 +38,9 @@ using InferenceTextToImageView = StabilityMatrix.Avalonia.Views.Inference.Infere
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceTextToImageView), persistent: true)]
public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
public partial class InferenceTextToImageViewModel
: InferenceTabViewModelBase,
IImageGalleryComponent
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -421,103 +423,100 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
notificationService.Show("No output", "Did not receive any output images");
return;
}
}
finally
{
// Disconnect progress handler
OutputProgress.Value = 0;
OutputProgress.Text = "";
ImageGalleryCardViewModel.PreviewImage?.Dispose();
ImageGalleryCardViewModel.PreviewImage = null;
ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false;
promptTask?.Dispose();
client.PreviewImageReceived -= OnPreviewImageReceived;
}
}
List<ImageSource> outputImages;
// Use local file path if available, otherwise use remote URL
if (client.OutputImagesDir is { } outputPath)
{
outputImages = new List<ImageSource>();
foreach (var image in images)
{
var filePath = image.ToFilePath(outputPath);
private async Task ProcessOutputs(IReadOnlyList<ComfyImage> images)
{
List<ImageSource> outputImages;
// Use local file path if available, otherwise use remote URL
if (client.OutputImagesDir is { } outputPath)
{
outputImages = new List<ImageSource>();
foreach (var image in images)
var bytesWithMetadata = PngDataHelper.AddMetadata(
await filePath.ReadAllBytesAsync(),
generationInfo,
smproj
);
/*await using (var readStream = filePath.Info.OpenWrite())
{
using (var reader = new BinaryReader(readStream))
{
}
}*/
await using (var outputStream = filePath.Info.OpenWrite())
{
await outputStream.WriteAsync(bytesWithMetadata);
await outputStream.FlushAsync();
}
outputImages.Add(new ImageSource(filePath));
imageIndexService.OnImageAdded(filePath);
}
}
else
{
var filePath = image.ToFilePath(outputPath);
outputImages = images!
.Select(i => new ImageSource(i.ToUri(client.BaseAddress)))
.ToList();
}
var bytesWithMetadata = PngDataHelper.AddMetadata(
await filePath.ReadAllBytesAsync(),
// Download all images to make grid, if multiple
if (outputImages.Count > 1)
{
var loadedImages = outputImages
.Select(i => SKImage.FromEncodedData(i.LocalFile?.Info.OpenRead()))
.ToImmutableArray();
var grid = ImageProcessor.CreateImageGrid(loadedImages);
var gridBytes = grid.Encode().ToArray();
var gridBytesWithMetadata = PngDataHelper.AddMetadata(
gridBytes,
generationInfo,
smproj
);
/*await using (var readStream = filePath.Info.OpenWrite())
{
using (var reader = new BinaryReader(readStream))
{
}
}*/
// Save to disk
var lastName = outputImages.Last().LocalFile?.Info.Name;
var gridPath = client.OutputImagesDir!.JoinFile($"grid-{lastName}");
await using (var outputStream = filePath.Info.OpenWrite())
await using (var fileStream = gridPath.Info.OpenWrite())
{
await outputStream.WriteAsync(bytesWithMetadata);
await outputStream.FlushAsync();
await fileStream.WriteAsync(gridBytesWithMetadata, cancellationToken);
}
outputImages.Add(new ImageSource(filePath));
// Insert to start of images
var gridImage = new ImageSource(gridPath);
// Preload
await gridImage.GetBitmapAsync();
ImageGalleryCardViewModel.ImageSources.Add(gridImage);
imageIndexService.OnImageAdded(filePath);
imageIndexService.OnImageAdded(gridPath);
}
}
else
{
outputImages = images!
.Select(i => new ImageSource(i.ToUri(client.BaseAddress)))
.ToList();
}
// Download all images to make grid, if multiple
if (outputImages.Count > 1)
{
var loadedImages = outputImages
.Select(i => SKImage.FromEncodedData(i.LocalFile?.Info.OpenRead()))
.ToImmutableArray();
var grid = ImageProcessor.CreateImageGrid(loadedImages);
var gridBytes = grid.Encode().ToArray();
var gridBytesWithMetadata = PngDataHelper.AddMetadata(
gridBytes,
generationInfo,
smproj
);
// Save to disk
var lastName = outputImages.Last().LocalFile?.Info.Name;
var gridPath = client.OutputImagesDir!.JoinFile($"grid-{lastName}");
await using (var fileStream = gridPath.Info.OpenWrite())
// Add rest of images
foreach (var img in outputImages)
{
await fileStream.WriteAsync(gridBytesWithMetadata, cancellationToken);
// Preload
await img.GetBitmapAsync();
ImageGalleryCardViewModel.ImageSources.Add(img);
}
// Insert to start of images
var gridImage = new ImageSource(gridPath);
// Preload
await gridImage.GetBitmapAsync();
ImageGalleryCardViewModel.ImageSources.Add(gridImage);
imageIndexService.OnImageAdded(gridPath);
}
// Add rest of images
foreach (var img in outputImages)
finally
{
// Preload
await img.GetBitmapAsync();
ImageGalleryCardViewModel.ImageSources.Add(img);
// Disconnect progress handler
OutputProgress.Value = 0;
OutputProgress.Text = "";
ImageGalleryCardViewModel.PreviewImage?.Dispose();
ImageGalleryCardViewModel.PreviewImage = null;
ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false;
promptTask?.Dispose();
client.PreviewImageReceived -= OnPreviewImageReceived;
}
}

37
StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs

@ -13,22 +13,21 @@ namespace StabilityMatrix.Avalonia.Views;
public partial class InferencePage : UserControlBase
{
private Button? _addButton;
private Button AddButton => _addButton
??= this.FindControl<TabView>("TabView")!
private Button AddButton =>
_addButton ??= this.FindControl<TabView>("TabView")!
.GetTemplateChildren()
.OfType<Button>()
.First(p => p.Name == "AddButton");
private readonly CommandBarFlyout addTabFlyout;
public InferencePage()
{
InitializeComponent();
AddHandler(DragDrop.DropEvent, DropHandler);
AddHandler(DragDrop.DragOverEvent, DragOverHandler);
addTabFlyout = Resources["AddTabFlyout"] as CommandBarFlyout
?? throw new NullReferenceException("AddTabFlyout not found");
addTabFlyout =
Resources["AddTabFlyout"] as CommandBarFlyout
?? throw new NullReferenceException("AddTabFlyout not found");
}
private void TabView_OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
@ -36,31 +35,15 @@ public partial class InferencePage : UserControlBase
(DataContext as InferenceViewModel)?.OnTabCloseRequested(args);
}
private void DragOverHandler(object? sender, DragEventArgs e)
{
if (DataContext is IDropTarget dropTarget)
{
dropTarget.DragOver(sender, e);
}
}
private void DropHandler(object? sender, DragEventArgs e)
{
if (DataContext is IDropTarget dropTarget)
{
dropTarget.Drop(sender, e);
}
}
private void TabView_OnAddTabButtonClick(TabView sender, EventArgs args)
{
ShowAddTabMenu(false);
}
private void ShowAddTabMenu(bool isTransient)
{
addTabFlyout.ShowMode = isTransient ? FlyoutShowMode.Transient : FlyoutShowMode.Standard;
addTabFlyout.ShowAt(AddButton);
}

28
StabilityMatrix.Core/Models/Database/LocalImageFile.cs

@ -54,6 +54,34 @@ public class LocalImageFile
return Path.Combine(rootImageDirectory, RelativePath);
}
public (
string? Parameters,
string? ParametersJson,
string? SMProject,
string? ComfyNodes
) ReadMetadata()
{
using var stream = new FileStream(
GlobalFullPath,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
using var reader = new BinaryReader(stream);
var parameters = ImageMetadata.ReadTextChunk(reader, "parameters");
var parametersJson = ImageMetadata.ReadTextChunk(reader, "parameters-json");
var smProject = ImageMetadata.ReadTextChunk(reader, "smproj");
var comfyNodes = ImageMetadata.ReadTextChunk(reader, "prompt");
return (
string.IsNullOrEmpty(parameters) ? null : parameters,
string.IsNullOrEmpty(parametersJson) ? null : parametersJson,
string.IsNullOrEmpty(smProject) ? null : smProject,
string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes
);
}
public static LocalImageFile FromPath(FilePath filePath)
{
var relativePath = Path.GetRelativePath(

Loading…
Cancel
Save