Multi-Platform Package Manager for Stable Diffusion
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

193 lines
5.8 KiB

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NLog;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Database;
using Size = System.Drawing.Size;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(SelectImageCard))]
[ManagedService]
[Transient]
public partial class SelectImageCardViewModel(INotificationService notificationService)
: LoadableViewModelBase,
IDropTarget,
IComfyStep,
IInputImageProvider
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsSelectionAvailable))]
[NotifyPropertyChangedFor(nameof(IsImageFileNotFound))]
private ImageSource? imageSource;
[ObservableProperty]
[property: JsonIgnore]
[NotifyPropertyChangedFor(nameof(IsSelectionAvailable))]
private bool isSelectionEnabled = true;
/// <summary>
/// Set by <see cref="SelectImageCard"/> when the image is loaded.
/// </summary>
[ObservableProperty]
private Size currentBitmapSize = Size.Empty;
/// <summary>
/// True if the image file is set but the local file does not exist.
/// </summary>
[MemberNotNullWhen(true, nameof(NotFoundImagePath))]
public bool IsImageFileNotFound => ImageSource?.LocalFile?.Exists == false;
public bool IsSelectionAvailable => IsSelectionEnabled && ImageSource == null;
/// <summary>
/// Path of the not found image
/// </summary>
public string? NotFoundImagePath => ImageSource?.LocalFile?.FullPath;
/// <inheritdoc />
public void ApplyStep(ModuleApplyStepEventArgs e)
{
e.Builder.SetupImagePrimarySource(
ImageSource ?? throw new ValidationException("Input Image is required"),
!CurrentBitmapSize.IsEmpty
? CurrentBitmapSize
: throw new ValidationException("CurrentBitmapSize is null"),
e.Builder.Connections.BatchIndex
);
}
/// <inheritdoc />
public IEnumerable<ImageSource> GetInputImages()
{
if (ImageSource is { } image && !IsImageFileNotFound)
{
yield return image;
}
}
partial void OnImageSourceChanged(ImageSource? value)
{
// Cache the hash for later upload use
if (value?.LocalFile is { Exists: true })
{
value.GetBlake3HashAsync().SafeFireAndForget();
}
}
private static FilePickerFileType SupportedImages { get; } =
new("Supported Images")
{
Patterns = new[] { "*.png", "*.jpg", "*.jpeg" },
AppleUniformTypeIdentifiers = new[] { "public.jpeg", "public.png" },
MimeTypes = new[] { "image/jpeg", "image/png" }
};
[RelayCommand]
private async Task SelectImageFromFilePickerAsync()
{
var files = await App.StorageProvider.OpenFilePickerAsync(
new FilePickerOpenOptions
{
FileTypeFilter = [FilePickerFileTypes.ImagePng, FilePickerFileTypes.ImageJpg, SupportedImages]
}
);
if (files.FirstOrDefault()?.TryGetLocalPath() is { } path)
{
Dispatcher.UIThread.Post(() => LoadUserImageSafe(new ImageSource(path)));
}
}
/// <summary>
/// Supports LocalImageFile Context or OS Files
/// </summary>
public void DragOver(object? sender, DragEventArgs e)
{
if (
e.Data.GetDataFormats().Contains(DataFormats.Files)
|| e.Data.GetContext<LocalImageFile>() is not null
)
{
e.Handled = true;
return;
}
e.DragEffects = DragDropEffects.None;
}
/// <inheritdoc />
public void Drop(object? sender, DragEventArgs e)
{
// 1. Context drop for LocalImageFile
if (e.Data.GetContext<LocalImageFile>() is { } imageFile)
{
e.Handled = true;
Dispatcher.UIThread.Post(() => LoadUserImageSafe(new ImageSource(imageFile.AbsolutePath)));
return;
}
// 2. OS Files
if (
e.Data.GetFiles() is { } files
&& files.Select(f => f.TryGetLocalPath()).FirstOrDefault() is { } path
)
{
e.Handled = true;
Dispatcher.UIThread.Post(() => LoadUserImageSafe(new ImageSource(path)));
}
}
/// <summary>
/// Calls <see cref="LoadUserImage"/> with notification error handling.
/// </summary>
private void LoadUserImageSafe(ImageSource image)
{
try
{
LoadUserImage(image);
}
catch (Exception e)
{
Logger.Warn(e, "Error loading image");
notificationService.Show("Error loading image", e.Message);
}
}
/// <summary>
/// Loads the user image from the given ImageSource.
/// </summary>
/// <param name="image">The ImageSource object representing the user image.</param>
[MethodImpl(MethodImplOptions.Synchronized)]
private void LoadUserImage(ImageSource image)
{
var current = ImageSource;
ImageSource = image;
current?.Dispose();
}
}