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.
 
 
 

402 lines
13 KiB

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
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.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(Views.OutputsPage))]
public partial class OutputsPageViewModel : PageViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
private readonly ILogger<OutputsPageViewModel> logger;
public override string Title => Resources.Label_OutputsPageTitle;
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
public SourceCache<OutputImageViewModel, string> OutputsCache { get; } =
new(p => p.ImageFile.AbsolutePath);
public IObservableCollection<OutputImageViewModel> Outputs { get; set; } =
new ObservableCollectionExtended<OutputImageViewModel>();
public IEnumerable<SharedOutputType> OutputTypes { get; } = Enum.GetValues<SharedOutputType>();
[ObservableProperty]
private ObservableCollection<PackageOutputCategory> categories;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanShowOutputTypes))]
private PackageOutputCategory selectedCategory;
[ObservableProperty]
private SharedOutputType selectedOutputType;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(NumImagesSelected))]
private int numItemsSelected;
[ObservableProperty]
private string searchQuery;
public bool CanShowOutputTypes =>
SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false;
public string NumImagesSelected =>
NumItemsSelected == 1
? Resources.Label_OneImageSelected
: string.Format(Resources.Label_NumImagesSelected, NumItemsSelected);
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
INotificationService notificationService,
INavigationService navigationService,
ILogger<OutputsPageViewModel> logger
)
{
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.navigationService = navigationService;
this.logger = logger;
var predicate = this.WhenPropertyChanged(vm => vm.SearchQuery)
.Throttle(TimeSpan.FromMilliseconds(50))!
.Select<PropertyValue<OutputsPageViewModel, string>, Func<OutputImageViewModel, bool>>(
propertyValue =>
output =>
{
if (string.IsNullOrWhiteSpace(propertyValue.Value))
return true;
return output.ImageFile.FileName.Contains(
propertyValue.Value,
StringComparison.OrdinalIgnoreCase
)
|| (
output.ImageFile.GenerationParameters?.PositivePrompt != null
&& output.ImageFile.GenerationParameters.PositivePrompt.Contains(
propertyValue.Value,
StringComparison.OrdinalIgnoreCase
)
);
}
)
.AsObservable();
OutputsCache
.Connect()
.DeferUntilLoaded()
.Filter(predicate)
.SortBy(x => x.ImageFile.CreatedAt, SortDirection.Descending)
.Bind(Outputs)
.WhenPropertyChanged(p => p.IsSelected)
.Subscribe(_ =>
{
NumItemsSelected = Outputs.Count(o => o.IsSelected);
});
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
if (!settingsManager.IsLibraryDirSet)
return;
Directory.CreateDirectory(settingsManager.ImagesDirectory);
var packageCategories = settingsManager.Settings.InstalledPackages
.Where(x => !x.UseSharedOutputFolder)
.Select(p =>
{
var basePackage = packageFactory[p.PackageName!];
if (basePackage is null)
return null;
return new PackageOutputCategory
{
Path = Path.Combine(p.FullPath, basePackage.OutputFolderName),
Name = p.DisplayName
};
})
.ToList();
packageCategories.Insert(
0,
new PackageOutputCategory
{
Path = settingsManager.ImagesDirectory,
Name = "Shared Output Folder"
}
);
Categories = new ObservableCollection<PackageOutputCategory>(packageCategories);
SelectedCategory = Categories.First();
SelectedOutputType = SharedOutputType.All;
SearchQuery = string.Empty;
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
}
partial void OnSelectedCategoryChanged(
PackageOutputCategory? oldValue,
PackageOutputCategory? newValue
)
{
if (oldValue == newValue || newValue == null)
return;
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
? Path.Combine(newValue.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
}
partial void OnSelectedOutputTypeChanged(SharedOutputType oldValue, SharedOutputType newValue)
{
if (oldValue == newValue)
return;
var path =
newValue == SharedOutputType.All
? SelectedCategory.Path
: Path.Combine(SelectedCategory.Path, newValue.ToString());
GetOutputs(path);
}
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.ImageFile.AbsolutePath));
// Preload
await image.GetBitmapAsync();
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile };
using var onNext = Observable
.FromEventPattern<DirectionalNavigationEventArgs>(
vm,
nameof(ImageViewerViewModel.NavigationRequested)
)
.Subscribe(ctx =>
{
Dispatcher.UIThread
.InvokeAsync(async () =>
{
var sender = (ImageViewerViewModel)ctx.Sender!;
var newIndex = currentIndex + (ctx.EventArgs.IsNext ? 1 : -1);
if (newIndex >= 0 && newIndex < Outputs.Count)
{
var newImage = Outputs[newIndex];
var newImageSource = new ImageSource(
new FilePath(newImage.ImageFile.AbsolutePath)
);
// Preload
await newImageSource.GetBitmapAsync();
sender.ImageSource = newImageSource;
sender.LocalImageFile = newImage.ImageFile;
currentIndex = newIndex;
}
})
.SafeFireAndForget();
});
await vm.GetDialog().ShowAsync();
}
public async Task CopyImage(string imagePath)
{
var clipboard = App.Clipboard;
await clipboard.SetFileDataObjectAsync(imagePath);
}
public async Task OpenImage(string imagePath) => await ProcessRunner.OpenFileBrowser(imagePath);
public async Task DeleteImage(OutputImageViewModel? item)
{
if (item is null)
return;
var confirmationDialog = new BetterContentDialog
{
Title = "Are you sure you want to delete this image?",
Content = "This action cannot be undone.",
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = true,
};
var dialogResult = await confirmationDialog.ShowAsync();
if (dialogResult != ContentDialogResult.Primary)
return;
// Delete the file
var imageFile = new FilePath(item.ImageFile.AbsolutePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
{
return;
}
OutputsCache.Remove(item);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
}
public void SendToTextToImage(OutputImageViewModel vm)
{
navigationService.NavigateTo<InferenceViewModel>();
EventManager.Instance.OnInferenceTextToImageRequested(vm.ImageFile);
}
public void SendToUpscale(OutputImageViewModel vm)
{
navigationService.NavigateTo<InferenceViewModel>();
EventManager.Instance.OnInferenceUpscaleRequested(vm.ImageFile);
}
public void ClearSelection()
{
foreach (var output in Outputs)
{
output.IsSelected = false;
}
}
public void SelectAll()
{
foreach (var output in Outputs)
{
output.IsSelected = true;
}
}
public async Task DeleteAllSelected()
{
var confirmationDialog = new BetterContentDialog
{
Title = $"Are you sure you want to delete {NumItemsSelected} images?",
Content = "This action cannot be undone.",
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = true,
};
var dialogResult = await confirmationDialog.ShowAsync();
if (dialogResult != ContentDialogResult.Primary)
return;
var selected = Outputs.Where(o => o.IsSelected).ToList();
Debug.Assert(selected.Count == NumItemsSelected);
foreach (var output in selected)
{
// Delete the file
var imageFile = new FilePath(output.ImageFile.AbsolutePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
{
continue;
}
OutputsCache.Remove(output);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
}
NumItemsSelected = 0;
ClearSelection();
}
private void GetOutputs(string directory)
{
if (!settingsManager.IsLibraryDirSet)
return;
if (!Directory.Exists(directory) && SelectedOutputType != SharedOutputType.All)
{
Directory.CreateDirectory(directory);
return;
}
var list = Directory
.EnumerateFiles(directory, "*.png", SearchOption.AllDirectories)
.Select(file => new OutputImageViewModel(LocalImageFile.FromPath(file)))
.OrderByDescending(f => f.ImageFile.CreatedAt);
OutputsCache.EditDiff(list, (x, y) => x.ImageFile.AbsolutePath == y.ImageFile.AbsolutePath);
}
}