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.

312 lines
9.4 KiB

using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.Logging;
using SkiaSharp;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using SortDirection = DynamicData.Binding.SortDirection;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(ImageFolderCard))]
public partial class ImageFolderCardViewModel : ViewModelBase
{
private readonly ILogger<ImageFolderCardViewModel> logger;
private readonly IImageIndexService imageIndexService;
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
/// <summary>
/// Source of image files to display
/// </summary>
private readonly SourceCache<LocalImageFile, string> localImagesSource =
new(imageFile => imageFile.RelativePath);
/// <summary>
/// Collection of image items to display
/// </summary>
public IObservableCollection<ImageFolderCardItemViewModel> Items { get; } =
new ObservableCollectionExtended<ImageFolderCardItemViewModel>();
/// <summary>
/// Collection of local image files
/// </summary>
public IObservableCollection<LocalImageFile> LocalImages { get; } =
new ObservableCollectionExtended<LocalImageFile>();
public ImageFolderCardViewModel(
ILogger<ImageFolderCardViewModel> logger,
IImageIndexService imageIndexService,
ISettingsManager settingsManager,
INotificationService notificationService
)
{
this.logger = logger;
this.imageIndexService = imageIndexService;
this.settingsManager = settingsManager;
this.notificationService = notificationService;
// var minDatetime = DateTimeOffset.FromUnixTimeMilliseconds(0);
/*localImagesSource
.Connect()
.DeferUntilLoaded()
.Transform(
imageFile =>
new ImageFolderCardItemViewModel
{
LocalImageFile = imageFile,
ImagePath = Design.IsDesignMode
? imageFile.RelativePath
: imageFile.GetFullPath(settingsManager.ImagesDirectory)
}
)
.SortBy(x => x.LocalImageFile?.LastModifiedAt ?? minDatetime, SortDirection.Descending)
.Bind(Items)
.Subscribe();*/
localImagesSource
.Connect()
.DeferUntilLoaded()
.SortBy(file => file.LastModifiedAt, SortDirection.Descending)
.Bind(LocalImages)
.Subscribe();
}
/// <inheritdoc />
public override async Task OnLoadedAsync()
{
await base.OnLoadedAsync();
await imageIndexService.RefreshIndex("Inference");
var imageFiles = await imageIndexService.GetLocalImagesByPrefix("Inference");
localImagesSource.Edit(x =>
{
x.Load(imageFiles);
});
}
/// <summary>
/// Handles image clicks to show preview
/// </summary>
[RelayCommand]
private async Task OnImageClick(LocalImageFile item)
{
if (item.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
return;
}
var image = new ImageSource(new FilePath(imagePath));
// Preload
await image.GetBitmapAsync();
var dialog = new BetterContentDialog
{
MaxDialogWidth = 1000,
MaxDialogHeight = 1000,
FullSizeDesired = true,
IsFooterVisible = false,
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
Content = new ImageViewerDialog
{
Width = 1000,
Height = 1000,
DataContext = new ImageViewerViewModel { ImageSource = image }
}
};
await dialog.ShowAsync();
}
/// <summary>
/// Handles clicks to the image delete button
/// </summary>
[RelayCommand]
private async Task OnImageDelete(LocalImageFile? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
return;
}
// Delete the file
var result = await notificationService.TryAsync(new FilePath(imagePath).DeleteAsync());
if (!result.IsSuccessful)
{
return;
}
// Remove from source
localImagesSource.Remove(item);
// Remove from index
await imageIndexService.RemoveImage(item);
}
/// <summary>
/// Handles clicks to the image delete button
/// </summary>
[RelayCommand]
private async Task OnImageCopy(LocalImageFile? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
return;
}
var clipboard = App.Clipboard;
var dataObject = new DataObject();
// TODO: Not working currently
dataObject.Set(DataFormats.Files, $"file:///{imagePath}");
await clipboard.SetDataObjectAsync(dataObject);
}
/// <summary>
/// Handles clicks to the image open-in-explorer button
/// </summary>
[RelayCommand]
private async Task OnImageOpen(LocalImageFile? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
return;
}
await ProcessRunner.OpenFileBrowser(imagePath);
}
/// <summary>
/// Handles clicks to the image export button
/// </summary>
private async Task ImageExportImpl(
LocalImageFile? item,
SKEncodedImageFormat format,
bool includeMetadata = false
)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } sourcePath)
{
return;
}
var sourceFile = new FilePath(sourcePath);
var formatName = format.ToString();
var storageFile = await App.StorageProvider.SaveFilePickerAsync(
new FilePickerSaveOptions
{
Title = "Export Image",
ShowOverwritePrompt = true,
SuggestedFileName = item.FileNameWithoutExtension,
DefaultExtension = formatName.ToLowerInvariant(),
FileTypeChoices = new FilePickerFileType[]
{
new(formatName)
{
Patterns = new[] { $"*.{formatName.ToLowerInvariant()}" },
MimeTypes = new[] { $"image/{formatName.ToLowerInvariant()}" }
}
}
}
);
if (storageFile?.TryGetLocalPath() is not { } targetPath)
{
return;
}
var targetFile = new FilePath(targetPath);
try
{
if (format is SKEncodedImageFormat.Png)
{
// For include metadata, just copy the file
if (includeMetadata)
{
await sourceFile.CopyToAsync(targetFile, true);
}
else
{
// Otherwise read and strip the metadata
var imageBytes = await sourceFile.ReadAllBytesAsync();
imageBytes = PngDataHelper.RemoveMetadata(imageBytes);
await targetFile.WriteAllBytesAsync(imageBytes);
}
}
else
{
await Task.Run(() =>
{
using var fs = sourceFile.Info.OpenRead();
var image = SKImage.FromEncodedData(fs);
fs.Dispose();
using var targetStream = targetFile.Info.OpenWrite();
image.Encode(format, 100).SaveTo(targetStream);
});
}
}
catch (IOException e)
{
logger.LogWarning(e, "Failed to export image");
notificationService.ShowPersistent(
"Failed to export image",
e.Message,
NotificationType.Error
);
return;
}
notificationService.Show(
"Image Exported",
$"Saved to {targetPath}",
NotificationType.Success
);
}
[RelayCommand]
private Task OnImageExportPng(LocalImageFile? item) =>
ImageExportImpl(item, SKEncodedImageFormat.Png);
[RelayCommand]
private Task OnImageExportPngWithMetadata(LocalImageFile? item) =>
ImageExportImpl(item, SKEncodedImageFormat.Png, true);
[RelayCommand]
private Task OnImageExportJpeg(LocalImageFile? item) =>
ImageExportImpl(item, SKEncodedImageFormat.Jpeg);
[RelayCommand]
private Task OnImageExportWebp(LocalImageFile? item) =>
ImageExportImpl(item, SKEncodedImageFormat.Webp);
}