diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml
index 0258b36b..fb90f374 100644
--- a/StabilityMatrix.Avalonia/App.axaml
+++ b/StabilityMatrix.Avalonia/App.axaml
@@ -60,6 +60,7 @@
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs b/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
new file mode 100644
index 00000000..3a1ca335
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
@@ -0,0 +1,25 @@
+using Avalonia;
+using Avalonia.Controls;
+
+namespace StabilityMatrix.Avalonia.Controls.SelectableImageCard;
+
+public class SelectableImageButton : Button
+{
+ public static readonly StyledProperty IsSelectedProperty =
+ CheckBox.IsCheckedProperty.AddOwner();
+
+ public static readonly StyledProperty SourceProperty =
+ BetterAdvancedImage.SourceProperty.AddOwner();
+
+ public bool? IsSelected
+ {
+ get => GetValue(IsSelectedProperty);
+ set => SetValue(IsSelectedProperty, value);
+ }
+
+ public string? Source
+ {
+ get => GetValue(SourceProperty);
+ set => SetValue(SourceProperty, value);
+ }
+}
diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
index 4b4d4599..068466b3 100644
--- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs
+++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
+using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
@@ -20,6 +21,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Inference;
+using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
@@ -29,6 +31,7 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
+using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
@@ -336,8 +339,27 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService();
- public static OutputsPageViewModel OutputsPageViewModel =>
- Services.GetRequiredService();
+ public static OutputsPageViewModel OutputsPageViewModel
+ {
+ get
+ {
+ var vm = Services.GetRequiredService();
+ vm.Outputs = new ObservableCollectionExtended
+ {
+ new(
+ new LocalImageFile
+ {
+ AbsolutePath =
+ "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
+ RelativePath =
+ "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
+ ImageType = LocalImageFileType.TextToImage
+ }
+ )
+ };
+ return vm;
+ }
+ }
public static PackageManagerViewModel PackageManagerViewModel
{
diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
index ab1c7e63..336977f1 100644
--- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
+++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
@@ -257,6 +257,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Open in Image Viewer.
+ ///
+ public static string Action_OpenInViewer {
+ get {
+ return ResourceManager.GetString("Action_OpenInViewer", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Open on CivitAI.
///
@@ -1157,6 +1166,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to {0} images selected.
+ ///
+ public static string Label_NumImagesSelected {
+ get {
+ return ResourceManager.GetString("Label_NumImagesSelected", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Only available on Windows.
///
diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx
index a0fd01c3..05d6781e 100644
--- a/StabilityMatrix.Avalonia/Languages/Resources.resx
+++ b/StabilityMatrix.Avalonia/Languages/Resources.resx
@@ -687,4 +687,10 @@
Copy
-
\ No newline at end of file
+
+ Open in Image Viewer
+
+
+ {0} images selected
+
+
diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs
new file mode 100644
index 00000000..1102d0ab
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs
@@ -0,0 +1,19 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Core.Models.Database;
+
+namespace StabilityMatrix.Avalonia.ViewModels.OutputsPage;
+
+public partial class OutputImageViewModel : ViewModelBase
+{
+ public LocalImageFile ImageFile { get; }
+
+ [ObservableProperty]
+ private bool isSelected;
+
+ public OutputImageViewModel(LocalImageFile imageFile)
+ {
+ ImageFile = imageFile;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
index 209fd099..4e07140d 100644
--- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
@@ -4,21 +4,24 @@ using System.Collections.ObjectModel;
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.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.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
@@ -32,21 +35,23 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
-[View(typeof(OutputsPage))]
+[View(typeof(Views.OutputsPage))]
public partial class OutputsPageViewModel : PageViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
+ private readonly ILogger logger;
public override string Title => "Outputs";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
- public SourceCache OutputsCache { get; } = new(p => p.AbsolutePath);
+ public SourceCache OutputsCache { get; } =
+ new(p => p.ImageFile.AbsolutePath);
- public IObservableCollection Outputs { get; } =
- new ObservableCollectionExtended();
+ public IObservableCollection Outputs { get; set; } =
+ new ObservableCollectionExtended();
public IEnumerable OutputTypes { get; } = Enum.GetValues();
@@ -60,25 +65,35 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty]
private SharedOutputType selectedOutputType;
+ [ObservableProperty]
+ private int numItemsSelected;
+
public bool CanShowOutputTypes => SelectedCategory.Name.Equals("Shared Output Folder");
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
INotificationService notificationService,
- INavigationService navigationService
+ INavigationService navigationService,
+ ILogger logger
)
{
this.settingsManager = settingsManager;
this.notificationService = notificationService;
this.navigationService = navigationService;
+ this.logger = logger;
OutputsCache
.Connect()
.DeferUntilLoaded()
- .SortBy(x => x.CreatedAt, SortDirection.Descending)
+ .SortBy(x => x.ImageFile.CreatedAt, SortDirection.Descending)
+ .ObserveOn(SynchronizationContext.Current)
.Bind(Outputs)
- .Subscribe();
+ .WhenPropertyChanged(p => p.IsSelected)
+ .Subscribe(_ =>
+ {
+ NumItemsSelected = Outputs.Count(o => o.IsSelected);
+ });
if (!settingsManager.IsLibraryDirSet || Design.IsDesignMode)
return;
@@ -152,16 +167,29 @@ public partial class OutputsPageViewModel : PageViewModelBase
GetOutputs(path);
}
- public async Task OnImageClick(LocalImageFile item)
+ 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.AbsolutePath));
+ var image = new ImageSource(new FilePath(item.ImageFile.AbsolutePath));
// Preload
await image.GetBitmapAsync();
- var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item };
+ var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile };
using var onNext = Observable
.FromEventPattern(
@@ -180,14 +208,14 @@ public partial class OutputsPageViewModel : PageViewModelBase
{
var newImage = Outputs[newIndex];
var newImageSource = new ImageSource(
- new FilePath(newImage.AbsolutePath)
+ new FilePath(newImage.ImageFile.AbsolutePath)
);
// Preload
await newImageSource.GetBitmapAsync();
sender.ImageSource = newImageSource;
- sender.LocalImageFile = newImage;
+ sender.LocalImageFile = newImage.ImageFile;
currentIndex = newIndex;
}
@@ -207,9 +235,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
public async Task OpenImage(string imagePath) => await ProcessRunner.OpenFileBrowser(imagePath);
- public async Task DeleteImage(LocalImageFile? item)
+ public async Task DeleteImage(OutputImageViewModel? item)
{
- if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
+ if (item?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
return;
}
@@ -223,7 +251,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
return;
}
- Outputs.Remove(item);
+ OutputsCache.Remove(item);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
@@ -244,6 +272,14 @@ public partial class OutputsPageViewModel : PageViewModelBase
EventManager.Instance.OnInferenceUpscaleRequested(image);
}
+ public void ClearSelection()
+ {
+ foreach (var output in Outputs)
+ {
+ output.IsSelected = false;
+ }
+ }
+
private void GetOutputs(string directory)
{
if (!settingsManager.IsLibraryDirSet)
@@ -257,9 +293,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
var list = Directory
.EnumerateFiles(directory, "*.png", SearchOption.AllDirectories)
- .Select(file => LocalImageFile.FromPath(file))
- .OrderByDescending(f => f.CreatedAt);
+ .Select(file => new OutputImageViewModel(LocalImageFile.FromPath(file)))
+ .OrderByDescending(f => f.ImageFile.CreatedAt);
- OutputsCache.EditDiff(list, (x, y) => x.AbsolutePath == y.AbsolutePath);
+ OutputsCache.EditDiff(list, (x, y) => x.ImageFile.AbsolutePath == y.ImageFile.AbsolutePath);
}
}
diff --git a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml
index 1c200b5d..32a86148 100644
--- a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml
@@ -13,7 +13,8 @@
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"
+ xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage"
+ xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}"
d:DesignHeight="450"
d:DesignWidth="700"
@@ -24,8 +25,9 @@
-
+
@@ -48,13 +50,31 @@
-
+
+ VerticalContentAlignment="Center" />
+
+
+
+
+
+
+
@@ -65,45 +85,45 @@
-
-
+
+