Browse Source

Merge pull request #290 from ionite34/shared-output

Shared Output Folder & Image Browser
pull/240/head
JT 1 year ago committed by GitHub
parent
commit
6df7be76e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      CHANGELOG.md
  2. 5
      StabilityMatrix.Avalonia/App.axaml.cs
  3. 3
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  4. 6
      StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs
  5. 18
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  6. 6
      StabilityMatrix.Avalonia/Languages/Resources.resx
  7. 7
      StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs
  8. 1
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  9. 78
      StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
  10. 265
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  11. 31
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  12. 14
      StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
  13. 145
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml
  14. 14
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs
  15. 10
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  16. 9
      StabilityMatrix.Core/Helper/EventManager.cs
  17. 29
      StabilityMatrix.Core/Helper/SharedFolders.cs
  18. 3
      StabilityMatrix.Core/Models/Database/LocalImageFile.cs
  19. 16
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  20. 1
      StabilityMatrix.Core/Models/InstalledPackage.cs
  21. 13
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  22. 32
      StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
  23. 11
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  24. 5
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  25. 8
      StabilityMatrix.Core/Models/Packages/DankDiffusion.cs
  26. 5
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  27. 5
      StabilityMatrix.Core/Models/Packages/FooocusMre.cs
  28. 7
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  29. 18
      StabilityMatrix.Core/Models/Packages/UnknownPackage.cs
  30. 13
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  31. 10
      StabilityMatrix.Core/Models/Packages/VoltaML.cs
  32. 12
      StabilityMatrix.Core/Models/SharedOutputType.cs

7
CHANGELOG.md

@ -5,7 +5,12 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.5.4
## v2.6.0
### Added
- Added "Output Sharing" option for all packages in the three-dots menu on the Packages page
- This will link the package's output folders to the relevant subfolders in the "Outputs" directory
- When a package only has a generic "outputs" folder, all generated images from that package will be linked to the "Outputs\Text2Img" folder when this option is enabled
- Added "Outputs" page for viewing generated images from any package, or the shared output folder
### Fixed
- Fixed crash when clicking Inference gallery image after the image is deleted externally in file explorer

5
StabilityMatrix.Avalonia/App.axaml.cs

@ -259,7 +259,8 @@ public sealed class App : Application
.AddSingleton<LaunchPageViewModel>()
.AddSingleton<ProgressManagerViewModel>()
.AddSingleton<InferenceViewModel>()
.AddSingleton<ProgressManagerViewModel>();
.AddSingleton<ProgressManagerViewModel>()
.AddSingleton<OutputsPageViewModel>();
services.AddSingleton<MainWindowViewModel>(
provider =>
@ -278,6 +279,7 @@ public sealed class App : Application
provider.GetRequiredService<PackageManagerViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>()
},
FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
}
@ -388,6 +390,7 @@ public sealed class App : Application
services.AddSingleton<ProgressManagerPage>();
services.AddSingleton<InferencePage>();
services.AddSingleton<NewCheckpointsPage>();
services.AddSingleton<OutputsPage>();
// Inference tabs
services.AddTransient<InferenceTextToImageView>();

3
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -336,6 +336,9 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static OutputsPageViewModel OutputsPageViewModel =>
Services.GetRequiredService<OutputsPageViewModel>();
public static PackageManagerViewModel PackageManagerViewModel
{
get

6
StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs

@ -27,16 +27,22 @@ public class MockImageIndexService : IImageIndexService
{
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/4a7e00a7-6f18-42d4-87c0-10e792df2640/width=1152",
AbsolutePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/4a7e00a7-6f18-42d4-87c0-10e792df2640/width=1152",
},
new()
{
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024",
AbsolutePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024",
},
new()
{
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152",
AbsolutePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152",
}
}
);

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

@ -140,6 +140,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Copy.
/// </summary>
public static string Action_Copy {
get {
return ResourceManager.GetString("Action_Copy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
@ -1526,6 +1535,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Output Sharing.
/// </summary>
public static string Label_UseSharedOutputFolder {
get {
return ResourceManager.GetString("Label_UseSharedOutputFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to VAE.
/// </summary>

6
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -678,7 +678,13 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restore Default Layout</value>
</data>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>Output Sharing</value>
</data>
<data name="Label_BatchIndex" xml:space="preserve">
<value>Batch Index</value>
</data>
<data name="Action_Copy" xml:space="preserve">
<value>Copy</value>
</data>
</root>

7
StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs

@ -0,0 +1,7 @@
namespace StabilityMatrix.Avalonia.Models;
public class PackageOutputCategory
{
public required string Name { get; set; }
public required string Path { get; set; }
}

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

@ -9,6 +9,7 @@ using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;

78
StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs

@ -111,6 +111,10 @@ public partial class InferenceViewModel : PageViewModelBase
// Keep RunningPackage updated with the current package pair
EventManager.Instance.RunningPackageStatusChanged += OnRunningPackageStatusChanged;
// "Send to Inference"
EventManager.Instance.InferenceTextToImageRequested += OnInferenceTextToImageRequested;
EventManager.Instance.InferenceUpscaleRequested += OnInferenceUpscaleRequested;
MenuSaveAsCommand.WithConditionalNotificationErrorHandler(notificationService);
MenuOpenProjectCommand.WithConditionalNotificationErrorHandler(notificationService);
}
@ -232,6 +236,16 @@ public partial class InferenceViewModel : PageViewModelBase
}
}
private void OnInferenceTextToImageRequested(object? sender, LocalImageFile e)
{
Dispatcher.UIThread.Post(() => AddTabFromImage(e).SafeFireAndForget());
}
private void OnInferenceUpscaleRequested(object? sender, LocalImageFile e)
{
Dispatcher.UIThread.Post(() => AddUpscalerTabFromImage(e).SafeFireAndForget());
}
/// <summary>
/// Update the database with current tabs
/// </summary>
@ -593,6 +607,70 @@ public partial class InferenceViewModel : PageViewModelBase
await SyncTabStatesWithDatabase();
}
private async Task AddTabFromImage(LocalImageFile imageFile)
{
var metadata = imageFile.ReadMetadata();
InferenceTabViewModelBase? vm = null;
if (!string.IsNullOrWhiteSpace(metadata.SMProject))
{
var document = JsonSerializer.Deserialize<InferenceProjectDocument>(metadata.SMProject);
if (document is null)
{
throw new ApplicationException(
"MenuOpenProject: Deserialize project file returned null"
);
}
if (document.State is null)
{
throw new ApplicationException("Project file does not have 'State' key");
}
document.VerifyVersion();
var textToImage = vmFactory.Get<InferenceTextToImageViewModel>();
textToImage.LoadStateFromJsonObject(document.State);
vm = textToImage;
}
else if (!string.IsNullOrWhiteSpace(metadata.Parameters))
{
if (GenerationParameters.TryParse(metadata.Parameters, out var generationParameters))
{
var textToImageViewModel = vmFactory.Get<InferenceTextToImageViewModel>();
textToImageViewModel.LoadStateFromParameters(generationParameters);
vm = textToImageViewModel;
}
}
if (vm == null)
{
notificationService.Show(
"Unable to load project from image",
"No image metadata found",
NotificationType.Error
);
return;
}
Tabs.Add(vm);
SelectedTab = vm;
await SyncTabStatesWithDatabase();
}
private async Task AddUpscalerTabFromImage(LocalImageFile imageFile)
{
var upscaleVm = vmFactory.Get<InferenceImageUpscaleViewModel>();
upscaleVm.IsUpscaleEnabled = true;
upscaleVm.SelectImageCardViewModel.ImageSource = new ImageSource(imageFile.AbsolutePath);
Tabs.Add(upscaleVm);
SelectedTab = upscaleVm;
await SyncTabStatesWithDatabase();
}
/// <summary>
/// Menu "Open Project" command.
/// </summary>

265
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
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.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(OutputsPage))]
public partial class OutputsPageViewModel : PageViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
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 IObservableCollection<LocalImageFile> Outputs { get; } =
new ObservableCollectionExtended<LocalImageFile>();
public IEnumerable<SharedOutputType> OutputTypes { get; } = Enum.GetValues<SharedOutputType>();
[ObservableProperty]
private ObservableCollection<PackageOutputCategory> categories;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanShowOutputTypes))]
private PackageOutputCategory selectedCategory;
[ObservableProperty]
private SharedOutputType selectedOutputType;
public bool CanShowOutputTypes => SelectedCategory.Name.Equals("Shared Output Folder");
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
INotificationService notificationService,
INavigationService navigationService
)
{
this.settingsManager = settingsManager;
this.notificationService = notificationService;
this.navigationService = navigationService;
OutputsCache
.Connect()
.DeferUntilLoaded()
.SortBy(x => x.CreatedAt, SortDirection.Descending)
.Bind(Outputs)
.Subscribe();
if (!settingsManager.IsLibraryDirSet || Design.IsDesignMode)
return;
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;
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
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(LocalImageFile item)
{
var currentIndex = Outputs.IndexOf(item);
var image = new ImageSource(new FilePath(item.AbsolutePath));
// Preload
await image.GetBitmapAsync();
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item };
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.AbsolutePath)
);
// Preload
await newImageSource.GetBitmapAsync();
sender.ImageSource = newImageSource;
sender.LocalImageFile = newImage;
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(LocalImageFile? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
return;
}
// Delete the file
var imageFile = new FilePath(imagePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
{
return;
}
Outputs.Remove(item);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
}
public void SendToTextToImage(LocalImageFile image)
{
navigationService.NavigateTo<InferenceViewModel>();
EventManager.Instance.OnInferenceTextToImageRequested(image);
}
public void SendToUpscale(LocalImageFile image)
{
navigationService.NavigateTo<InferenceViewModel>();
EventManager.Instance.OnInferenceUpscaleRequested(image);
}
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 => LocalImageFile.FromPath(file))
.OrderByDescending(f => f.CreatedAt);
OutputsCache.EditDiff(list, (x, y) => x.AbsolutePath == y.AbsolutePath);
}
}

31
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs

@ -62,6 +62,9 @@ public partial class PackageCardViewModel : ProgressViewModel
[ObservableProperty]
private bool canUseConfigMethod;
[ObservableProperty]
private bool useSharedOutput;
public PackageCardViewModel(
ILogger<PackageCardViewModel> logger,
IPackageFactory packageFactory,
@ -100,6 +103,7 @@ public partial class PackageCardViewModel : ProgressViewModel
CanUseConfigMethod =
basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration)
?? false;
UseSharedOutput = Package?.UseSharedOutputFolder ?? false;
}
}
@ -367,6 +371,33 @@ public partial class PackageCardViewModel : ProgressViewModel
public void ToggleSharedModelNone() => IsSharedModelDisabled = !IsSharedModelDisabled;
public void ToggleSharedOutput() => UseSharedOutput = !UseSharedOutput;
partial void OnUseSharedOutputChanged(bool value)
{
if (Package == null)
return;
if (value == Package.UseSharedOutputFolder)
return;
using var st = settingsManager.BeginTransaction();
Package.UseSharedOutputFolder = value;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
return;
if (value)
{
basePackage.SetupOutputFolderLinks(Package.FullPath!);
}
else
{
basePackage.RemoveOutputFolderLinks(Package.FullPath!);
}
}
// fake radio button stuff
partial void OnIsSharedModelSymlinkChanged(bool oldValue, bool newValue)
{

14
StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml

@ -106,6 +106,20 @@
Grid.Row="1"
Text="{Binding NegativePrompt}" />
</Grid>
<Grid RowDefinitions="Auto,*">
<TextBlock Text="Model" Theme="{DynamicResource BodyStrongTextBlockStyle}" />
<SelectableTextBlock
Grid.Row="1"
Text="{Binding ModelName}" />
</Grid>
<Grid RowDefinitions="Auto,*">
<TextBlock Text="Seed" Theme="{DynamicResource BodyStrongTextBlockStyle}" />
<SelectableTextBlock
Grid.Row="1"
Text="{Binding Seed}" />
</Grid>
</StackPanel>
</ui:TeachingTip>

145
StabilityMatrix.Avalonia/Views/OutputsPage.axaml

@ -0,0 +1,145 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.OutputsPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
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"
d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}"
d:DesignHeight="450"
d:DesignWidth="700"
x:CompileBindings="True"
x:DataType="vm:OutputsPageViewModel"
mc:Ignorable="d">
<Grid RowDefinitions="Auto, *" Margin="16">
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,16"
HorizontalAlignment="Left">
<Grid RowDefinitions="Auto, *">
<TextBlock Text="Output Folder" Margin="4"/>
<ComboBox Grid.Row="1" ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
MinWidth="150">
<ComboBox.Styles>
<Style
Selector="ComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ComboBox.Styles>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models1:PackageOutputCategory}">
<StackPanel>
<TextBlock
Margin="0,4,0,4"
Text="{Binding Name, Mode=OneWay}" />
<TextBlock Text="{Binding Path, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
<Grid RowDefinitions="Auto, *" Margin="8,0"
IsVisible="{Binding CanShowOutputTypes}">
<TextBlock Text="Output Type" Margin="4"/>
<ComboBox Grid.Row="1" ItemsSource="{Binding OutputTypes}"
SelectedItem="{Binding SelectedOutputType}"
MinWidth="150"
VerticalAlignment="Stretch"
VerticalContentAlignment="Center"/>
</Grid>
</StackPanel>
<ScrollViewer Grid.Row="1">
<ItemsRepeater
ItemsSource="{Binding Outputs}"
VerticalAlignment="Top">
<ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="16" MinRowSpacing="16" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type database:LocalImageFile}">
<Button
Margin="0"
Padding="4"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).OnImageClick}"
CommandParameter="{Binding }">
<controls:BetterAdvancedImage
Width="300"
Height="300"
Stretch="UniformToFill"
Source="{Binding AbsolutePath}" />
<Button.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}" />
<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}" />
<ui:MenuFlyoutSeparator
IsVisible="{Binding GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ui:MenuFlyoutSubItem Text="Send to Inference" IconSource="Share"
IsVisible="{Binding GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}">
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="Text to Image"
IconSource="FullScreenMaximize"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).SendToTextToImage}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="Image to Image"
IsEnabled="False"
IconSource="ImageCopy"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
Text="Inpainting"
IconSource="ImageEdit"
IsEnabled="False"
HotKey="{x:Null}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
Text="Upscale"
HotKey="{x:Null}"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).SendToUpscale}"
CommandParameter="{Binding }">
<ui:MenuFlyoutItem.IconSource>
<fluentAvalonia:SymbolIconSource
FontSize="10"
Symbol="ResizeImage" />
</ui:MenuFlyoutItem.IconSource>
</ui:MenuFlyoutItem>
</ui:MenuFlyoutSubItem>
</ui:FAMenuFlyout>
</Button.ContextFlyout>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</controls:UserControlBase>

14
StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Views;
public partial class OutputsPage : UserControlBase
{
public OutputsPage()
{
InitializeComponent();
}
}

10
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -97,6 +97,16 @@
</MenuItem>
<!-- ReSharper enable Xaml.RedundantResource -->
</MenuItem>
<MenuItem
Header="{x:Static lang:Resources.Label_UseSharedOutputFolder}"
Command="{Binding ToggleSharedOutput}">
<MenuItem.Icon>
<CheckBox Margin="8,0,0,0"
Padding="0"
Width="28" Height="28"
IsChecked="{Binding UseSharedOutput}"/>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="{x:Static lang:Resources.Action_Uninstall}"
Command="{Binding Uninstall}">

9
StabilityMatrix.Core/Helper/EventManager.cs

@ -1,5 +1,6 @@
using System.Globalization;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
@ -34,6 +35,8 @@ public class EventManager
public event EventHandler? ModelIndexChanged;
public event EventHandler<FilePath>? ImageFileAdded;
public event EventHandler<LocalImageFile>? InferenceTextToImageRequested;
public event EventHandler<LocalImageFile>? InferenceUpscaleRequested;
public void OnGlobalProgressChanged(int progress) =>
GlobalProgressChanged?.Invoke(this, progress);
@ -74,4 +77,10 @@ public class EventManager
public void OnModelIndexChanged() => ModelIndexChanged?.Invoke(this, EventArgs.Empty);
public void OnImageFileAdded(FilePath filePath) => ImageFileAdded?.Invoke(this, filePath);
public void OnInferenceTextToImageRequested(LocalImageFile imageFile) =>
InferenceTextToImageRequested?.Invoke(this, imageFile);
public void OnInferenceUpscaleRequested(LocalImageFile imageFile) =>
InferenceUpscaleRequested?.Invoke(this, imageFile);
}

29
StabilityMatrix.Core/Helper/SharedFolders.cs

@ -45,10 +45,12 @@ public class SharedFolders : ISharedFolders
/// <param name="sourceDir">Shared source (i.e. "Models/")</param>
/// <param name="destinationDir">Destination (i.e. "webui/models/lora")</param>
/// <param name="overwrite">Whether to overwrite the destination if it exists</param>
/// <param name="recursiveDelete">Whether to recursively delete the directory after moving data out of it</param>
public static async Task CreateOrUpdateLink(
DirectoryPath sourceDir,
DirectoryPath destinationDir,
bool overwrite = false
bool overwrite = false,
bool recursiveDelete = false
)
{
// Create source folder if it doesn't exist
@ -74,6 +76,7 @@ public class SharedFolders : ISharedFolders
// Otherwise delete the link
Logger.Info($"Deleting existing junction at target {destinationDir}");
destinationDir.Info.Attributes = FileAttributes.Normal;
await destinationDir.DeleteAsync(false).ConfigureAwait(false);
}
else
@ -93,7 +96,7 @@ public class SharedFolders : ISharedFolders
}
Logger.Info($"Deleting existing empty folder at target {destinationDir}");
await destinationDir.DeleteAsync(false).ConfigureAwait(false);
await destinationDir.DeleteAsync(recursiveDelete).ConfigureAwait(false);
}
}
@ -117,11 +120,13 @@ public class SharedFolders : ISharedFolders
/// Updates or creates shared links for a package.
/// Will attempt to move files from the destination to the source if the destination is not empty.
/// </summary>
public static async Task UpdateLinksForPackage(
Dictionary<SharedFolderType, IReadOnlyList<string>> sharedFolders,
public static async Task UpdateLinksForPackage<T>(
Dictionary<T, IReadOnlyList<string>> sharedFolders,
DirectoryPath modelsDirectory,
DirectoryPath installDirectory
DirectoryPath installDirectory,
bool recursiveDelete = false
)
where T : Enum
{
foreach (var (folderType, relativePaths) in sharedFolders)
{
@ -130,14 +135,22 @@ public class SharedFolders : ISharedFolders
var sourceDir = new DirectoryPath(modelsDirectory, folderType.GetStringValue());
var destinationDir = installDirectory.JoinDir(relativePath);
await CreateOrUpdateLink(sourceDir, destinationDir).ConfigureAwait(false);
await CreateOrUpdateLink(
sourceDir,
destinationDir,
recursiveDelete: recursiveDelete
)
.ConfigureAwait(false);
}
}
}
public static void RemoveLinksForPackage(BasePackage package, DirectoryPath installPath)
public static void RemoveLinksForPackage<T>(
Dictionary<T, IReadOnlyList<string>>? sharedFolders,
DirectoryPath installPath
)
where T : Enum
{
var sharedFolders = package.SharedFolders;
if (sharedFolders == null)
{
return;

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

@ -17,6 +17,8 @@ public class LocalImageFile
[BsonId]
public required string RelativePath { get; set; }
public required string AbsolutePath { get; set; }
/// <summary>
/// Type of the model file.
/// </summary>
@ -127,6 +129,7 @@ public class LocalImageFile
return new LocalImageFile
{
RelativePath = relativePath,
AbsolutePath = filePath,
ImageType = imageType,
CreatedAt = filePath.Info.CreationTimeUtc,
LastModifiedAt = filePath.Info.LastWriteTimeUtc,

16
StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs

@ -161,7 +161,21 @@ public class FilePath : FileSystemPath, IPathObject
/// </summary>
public async Task<FilePath> MoveToAsync(FilePath destinationFile)
{
await Task.Run(() => Info.MoveTo(destinationFile.FullPath)).ConfigureAwait(false);
await Task.Run(() =>
{
var path = destinationFile.FullPath;
if (destinationFile.Exists)
{
var num = Random.Shared.NextInt64(0, 10000);
path = path.Replace(
destinationFile.NameWithoutExtension,
$"{destinationFile.NameWithoutExtension}_{num}"
);
}
Info.MoveTo(path);
})
.ConfigureAwait(false);
// Return the new path
return destinationFile;
}

1
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -50,6 +50,7 @@ public class InstalledPackage : IJsonOnDeserialized
public bool UpdateAvailable { get; set; }
public TorchVersion? PreferredTorchVersion { get; set; }
public SharedFolderMethod? PreferredSharedFolderMethod { get; set; }
public bool UseSharedOutputFolder { get; set; }
/// <summary>
/// Get the launch args host option value.

13
StabilityMatrix.Core/Models/Packages/A3WebUI.cs

@ -61,6 +61,17 @@ public class A3WebUI : BaseGitPackage
[SharedFolderType.AfterDetailer] = new[] { "models/adetailer" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new()
{
[SharedOutputType.Extras] = new[] { "outputs/extras-images" },
[SharedOutputType.Saved] = new[] { "log/images" },
[SharedOutputType.Img2Img] = new[] { "outputs/img2img-images" },
[SharedOutputType.Text2Img] = new[] { "outputs/text2img-images" },
[SharedOutputType.Img2ImgGrids] = new[] { "outputs/img2img-grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "outputs/text2img-grids" }
};
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
@ -165,6 +176,8 @@ public class A3WebUI : BaseGitPackage
return release.TagName!;
}
public override string OutputFolderName => "outputs";
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,

32
StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs

@ -372,7 +372,37 @@ public abstract class BaseGitPackage : BasePackage
{
if (SharedFolders is not null && sharedFolderMethod == SharedFolderMethod.Symlink)
{
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(this, installDirectory);
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(
SharedFolders,
installDirectory
);
}
return Task.CompletedTask;
}
public override Task SetupOutputFolderLinks(DirectoryPath installDirectory)
{
if (SharedOutputFolders is { } sharedOutputFolders)
{
return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(
sharedOutputFolders,
SettingsManager.ImagesDirectory,
installDirectory,
recursiveDelete: true
);
}
return Task.CompletedTask;
}
public override Task RemoveOutputFolderLinks(DirectoryPath installDirectory)
{
if (SharedOutputFolders is { } sharedOutputFolders)
{
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(
sharedOutputFolders,
installDirectory
);
}
return Task.CompletedTask;
}

11
StabilityMatrix.Core/Models/Packages/BasePackage.cs

@ -38,6 +38,8 @@ public abstract class BasePackage
public virtual bool IsInferenceCompatible => false;
public abstract string OutputFolderName { get; }
public abstract Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
@ -93,6 +95,9 @@ public abstract class BasePackage
SharedFolderMethod sharedFolderMethod
);
public abstract Task SetupOutputFolderLinks(DirectoryPath installDirectory);
public abstract Task RemoveOutputFolderLinks(DirectoryPath installDirectory);
public abstract IEnumerable<TorchVersion> AvailableTorchVersions { get; }
public virtual TorchVersion GetRecommendedTorchVersion()
@ -142,7 +147,11 @@ public abstract class BasePackage
/// The shared folders that this package supports.
/// Mapping of <see cref="SharedFolderType"/> to the relative paths from the package root.
/// </summary>
public virtual Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Dictionary<
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public abstract Task<string> GetLatestVersion();
public abstract Task<PackageVersionOptions> GetAllVersionOptions();

5
StabilityMatrix.Core/Models/Packages/ComfyUI.cs

@ -30,6 +30,8 @@ public class ComfyUI : BaseGitPackage
new("https://github.com/comfyanonymous/ComfyUI/raw/master/comfyui_screenshot.png");
public override bool ShouldIgnoreReleases => true;
public override bool IsInferenceCompatible => true;
public override string OutputFolderName => "output";
public override SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Configuration;
@ -58,6 +60,9 @@ public class ComfyUI : BaseGitPackage
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" },
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "output" } };
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{

8
StabilityMatrix.Core/Models/Packages/DankDiffusion.cs

@ -30,6 +30,8 @@ public class DankDiffusion : BaseGitPackage
public override Uri PreviewImageUri { get; }
public override string OutputFolderName { get; }
public override Task RunPackage(
string installedPackagePath,
string command,
@ -68,6 +70,12 @@ public class DankDiffusion : BaseGitPackage
public override List<LaunchOptionDefinition> LaunchOptions { get; }
public override Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public override Dictionary<
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public override Task<string> GetLatestVersion()
{
throw new NotImplementedException();

5
StabilityMatrix.Core/Models/Packages/Fooocus.cs

@ -83,6 +83,9 @@ public class Fooocus : BaseGitPackage
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "outputs" } };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm };
@ -90,6 +93,8 @@ public class Fooocus : BaseGitPackage
public override bool ShouldIgnoreReleases => true;
public override string OutputFolderName => "outputs";
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,

5
StabilityMatrix.Core/Models/Packages/FooocusMre.cs

@ -85,6 +85,9 @@ public class FooocusMre : BaseGitPackage
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "outputs" } };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm };
@ -94,6 +97,8 @@ public class FooocusMre : BaseGitPackage
return release.TagName!;
}
public override string OutputFolderName => "outputs";
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,

7
StabilityMatrix.Core/Models/Packages/InvokeAI.cs

@ -69,6 +69,11 @@ public class InvokeAI : BaseGitPackage
[SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" },
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "invokeai-root/outputs/images" } };
public override string OutputFolderName => "invokeai-root/outputs/images";
// https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
@ -235,8 +240,6 @@ public class InvokeAI : BaseGitPackage
? $"https://github.com/invoke-ai/InvokeAI/archive/{latestVersion}.zip"
: $"https://github.com/invoke-ai/InvokeAI/archive/refs/heads/{installedPackage.Version.InstalledBranch}.zip";
var gpus = HardwareHelper.IterGpuInfo().ToList();
progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true));
var pipCommandArgs =

18
StabilityMatrix.Core/Models/Packages/UnknownPackage.cs

@ -26,6 +26,8 @@ public class UnknownPackage : BasePackage
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override string OutputFolderName { get; }
public override Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
@ -83,6 +85,16 @@ public class UnknownPackage : BasePackage
throw new NotImplementedException();
}
public override Task SetupOutputFolderLinks(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
public override Task RemoveOutputFolderLinks(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cuda, TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl };
@ -122,6 +134,12 @@ public class UnknownPackage : BasePackage
public override List<LaunchOptionDefinition> LaunchOptions => new();
public override Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public override Dictionary<
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public override Task<string> GetLatestVersion() => Task.FromResult(string.Empty);
public override Task<PackageVersionOptions> GetAllVersionOptions() =>

13
StabilityMatrix.Core/Models/Packages/VladAutomatic.cs

@ -67,6 +67,19 @@ public class VladAutomatic : BaseGitPackage
[SharedFolderType.ControlNet] = new[] { "models/ControlNet" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new()
{
[SharedOutputType.Text2Img] = new[] { "outputs/text" },
[SharedOutputType.Img2Img] = new[] { "outputs/image" },
[SharedOutputType.Extras] = new[] { "outputs/extras" },
[SharedOutputType.Img2ImgGrids] = new[] { "outputs/grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "outputs/grids" },
[SharedOutputType.Saved] = new[] { "outputs/save" },
};
public override string OutputFolderName => "outputs";
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions =>
new()

10
StabilityMatrix.Core/Models/Packages/VoltaML.cs

@ -45,6 +45,16 @@ public class VoltaML : BaseGitPackage
[SharedFolderType.TextualInversion] = new[] { "data/textual-inversion" },
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new()
{
[SharedOutputType.Text2Img] = new[] { "data/outputs/txt2img" },
[SharedOutputType.Extras] = new[] { "data/outputs/extra" },
[SharedOutputType.Img2Img] = new[] { "data/outputs/img2img" },
};
public override string OutputFolderName => "data/outputs";
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable<TorchVersion> AvailableTorchVersions => new[] { TorchVersion.None };

12
StabilityMatrix.Core/Models/SharedOutputType.cs

@ -0,0 +1,12 @@
namespace StabilityMatrix.Core.Models;
public enum SharedOutputType
{
All,
Text2Img,
Img2Img,
Extras,
Text2ImgGrids,
Img2ImgGrids,
Saved
}
Loading…
Cancel
Save