Ionite
1 year ago
22 changed files with 702 additions and 128 deletions
@ -0,0 +1,62 @@
|
||||
<Styles |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" |
||||
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" |
||||
x:DataType="vmInference:ImageFolderCardViewModel"> |
||||
|
||||
<Design.PreviewWith> |
||||
<Panel Width="600" Height="800"> |
||||
<Grid MaxWidth="500" MaxHeight="700"> |
||||
<controls:ImageFolderCard |
||||
DataContext="{x:Static mocks:DesignData.ImageFolderCardViewModel}" /> |
||||
</Grid> |
||||
</Panel> |
||||
</Design.PreviewWith> |
||||
|
||||
|
||||
<!--<Style Selector="ListBox /template/ VirtualizingStackPanel"> |
||||
<Setter Property="Orientation" Value="Horizontal" /> |
||||
</Style>--> |
||||
|
||||
<Style Selector="controls|ImageFolderCard"> |
||||
<!-- Set Defaults --> |
||||
<Setter Property="Template"> |
||||
<ControlTemplate> |
||||
<controls:Card |
||||
Padding="8" |
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}" |
||||
HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}" |
||||
VerticalContentAlignment="{TemplateBinding VerticalAlignment}"> |
||||
|
||||
<ScrollViewer> |
||||
<ItemsRepeater |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Stretch" |
||||
ItemsSource="{Binding Items}"> |
||||
<ItemsRepeater.Layout> |
||||
<UniformGridLayout |
||||
MinColumnSpacing="8" |
||||
MinRowSpacing="8" |
||||
MaximumRowsOrColumns="2"/> |
||||
</ItemsRepeater.Layout> |
||||
<ItemsRepeater.ItemTemplate> |
||||
<DataTemplate DataType="{x:Type vmInference:ImageFolderCardItemViewModel}"> |
||||
<controls:BetterAdvancedImage |
||||
MaxHeight="256" |
||||
CornerRadius="4" |
||||
Source="{Binding ImagePath}" |
||||
Stretch="Uniform" |
||||
StretchDirection="Both" /> |
||||
</DataTemplate> |
||||
</ItemsRepeater.ItemTemplate> |
||||
</ItemsRepeater> |
||||
</ScrollViewer> |
||||
</controls:Card> |
||||
</ControlTemplate> |
||||
</Setter> |
||||
</Style> |
||||
</Styles> |
@ -0,0 +1,27 @@
|
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls.Primitives; |
||||
using Avalonia.Interactivity; |
||||
using Avalonia.Threading; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
public class ImageFolderCard : TemplatedControl |
||||
{ |
||||
/// <inheritdoc /> |
||||
protected override void OnLoaded(RoutedEventArgs e) |
||||
{ |
||||
base.OnLoaded(e); |
||||
|
||||
if (DataContext is ViewModelBase vm) |
||||
{ |
||||
vm.OnLoaded(); |
||||
Dispatcher.UIThread |
||||
.InvokeAsync(async () => |
||||
{ |
||||
await vm.OnLoadedAsync(); |
||||
}) |
||||
.SafeFireAndForget(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic; |
||||
using System.Threading.Tasks; |
||||
using StabilityMatrix.Core.Models.Database; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.DesignData; |
||||
|
||||
public class MockImageIndexService : IImageIndexService |
||||
{ |
||||
/// <inheritdoc /> |
||||
public Task<IReadOnlyList<LocalImageFile>> GetLocalImagesByPrefix(string pathPrefix) |
||||
{ |
||||
return Task.FromResult( |
||||
(IReadOnlyList<LocalImageFile>) |
||||
new LocalImageFile[] |
||||
{ |
||||
new() |
||||
{ |
||||
RelativePath = |
||||
"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", |
||||
}, |
||||
new() |
||||
{ |
||||
RelativePath = |
||||
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152", |
||||
} |
||||
} |
||||
); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public Task RefreshIndex(string subPath = "") |
||||
{ |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void BackgroundRefreshIndex() |
||||
{ |
||||
throw new System.NotImplementedException(); |
||||
} |
||||
} |
@ -0,0 +1,14 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Models.Database; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Inference; |
||||
|
||||
public partial class ImageFolderCardItemViewModel : ViewModelBase |
||||
{ |
||||
[ObservableProperty] |
||||
private LocalImageFile? localImageFile; |
||||
|
||||
[ObservableProperty] |
||||
private string? imagePath; |
||||
} |
@ -0,0 +1,83 @@
|
||||
using System; |
||||
using System.Reactive.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using Avalonia.Controls; |
||||
using DynamicData; |
||||
using DynamicData.Binding; |
||||
using Microsoft.Extensions.Logging; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Models.Database; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
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; |
||||
|
||||
/// <summary> |
||||
/// Source of image files to display |
||||
/// </summary> |
||||
private readonly SourceCache<LocalImageFile, string> localImagesSource = |
||||
new(imageFile => imageFile.RelativePath); |
||||
|
||||
/// <summary> |
||||
/// Collection of image files to display |
||||
/// </summary> |
||||
public IObservableCollection<LocalImageFile> LocalImages { get; } = |
||||
new ObservableCollectionExtended<LocalImageFile>(); |
||||
|
||||
/// <summary> |
||||
/// Collection of image items to display |
||||
/// </summary> |
||||
public IObservableCollection<ImageFolderCardItemViewModel> Items { get; } = |
||||
new ObservableCollectionExtended<ImageFolderCardItemViewModel>(); |
||||
|
||||
public ImageFolderCardViewModel( |
||||
ILogger<ImageFolderCardViewModel> logger, |
||||
IImageIndexService imageIndexService, |
||||
ISettingsManager settingsManager |
||||
) |
||||
{ |
||||
this.logger = logger; |
||||
this.imageIndexService = imageIndexService; |
||||
this.settingsManager = settingsManager; |
||||
|
||||
localImagesSource |
||||
.Connect() |
||||
.DeferUntilLoaded() |
||||
.Transform( |
||||
imageFile => |
||||
new ImageFolderCardItemViewModel |
||||
{ |
||||
LocalImageFile = imageFile, |
||||
ImagePath = Design.IsDesignMode |
||||
? imageFile.RelativePath |
||||
: imageFile.GetFullPath(settingsManager.ImagesDirectory) |
||||
} |
||||
) |
||||
.Bind(Items) |
||||
.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); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,48 @@
|
||||
using LiteDB; |
||||
|
||||
namespace StabilityMatrix.Core.Models.Database; |
||||
|
||||
/// <summary> |
||||
/// Represents a locally indexed image file. |
||||
/// </summary> |
||||
public class LocalImageFile |
||||
{ |
||||
/// <summary> |
||||
/// Relative path of the file from the root images directory ("%LIBRARY%/Images"). |
||||
/// </summary> |
||||
[BsonId] |
||||
public required string RelativePath { get; set; } |
||||
|
||||
/// <summary> |
||||
/// Type of the model file. |
||||
/// </summary> |
||||
public LocalImageFileType ImageType { get; set; } |
||||
|
||||
/// <summary> |
||||
/// Creation time of the file. |
||||
/// </summary> |
||||
public DateTimeOffset CreatedAt { get; set; } |
||||
|
||||
/// <summary> |
||||
/// Last modified time of the file. |
||||
/// </summary> |
||||
public DateTimeOffset LastModifiedAt { get; set; } |
||||
|
||||
/// <summary> |
||||
/// File name of the relative path. |
||||
/// </summary> |
||||
public string FileName => Path.GetFileName(RelativePath); |
||||
|
||||
/// <summary> |
||||
/// File name of the relative path without extension. |
||||
/// </summary> |
||||
public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(RelativePath); |
||||
|
||||
public string GetFullPath(string rootImageDirectory) |
||||
{ |
||||
return Path.Combine(rootImageDirectory, RelativePath); |
||||
} |
||||
|
||||
public static readonly HashSet<string> SupportedImageExtensions = |
||||
new() { ".png", ".jpg", ".jpeg", ".webp" }; |
||||
} |
@ -0,0 +1,14 @@
|
||||
namespace StabilityMatrix.Core.Models.Database; |
||||
|
||||
[Flags] |
||||
public enum LocalImageFileType : ulong |
||||
{ |
||||
// Source |
||||
Automatic = 1 << 1, |
||||
Comfy = 1 << 2, |
||||
Inference = 1 << 3, |
||||
|
||||
// Generation Type |
||||
TextToImage = 1 << 10, |
||||
ImageToImage = 1 << 11 |
||||
} |
@ -0,0 +1,21 @@
|
||||
using StabilityMatrix.Core.Models.Database; |
||||
|
||||
namespace StabilityMatrix.Core.Services; |
||||
|
||||
public interface IImageIndexService |
||||
{ |
||||
/// <summary> |
||||
/// Gets a list of local images that start with the given path prefix |
||||
/// </summary> |
||||
Task<IReadOnlyList<LocalImageFile>> GetLocalImagesByPrefix(string pathPrefix); |
||||
|
||||
/// <summary> |
||||
/// Refreshes the index of local images |
||||
/// </summary> |
||||
Task RefreshIndex(string subPath = ""); |
||||
|
||||
/// <summary> |
||||
/// Refreshes the index of local images in the background |
||||
/// </summary> |
||||
void BackgroundRefreshIndex(); |
||||
} |
@ -0,0 +1,111 @@
|
||||
using System.Diagnostics; |
||||
using AsyncAwaitBestPractices; |
||||
using Microsoft.Extensions.Logging; |
||||
using StabilityMatrix.Core.Database; |
||||
using StabilityMatrix.Core.Models.Database; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
|
||||
namespace StabilityMatrix.Core.Services; |
||||
|
||||
public class ImageIndexService : IImageIndexService |
||||
{ |
||||
private readonly ILogger<ImageIndexService> logger; |
||||
private readonly ILiteDbContext liteDbContext; |
||||
private readonly ISettingsManager settingsManager; |
||||
|
||||
public ImageIndexService( |
||||
ILogger<ImageIndexService> logger, |
||||
ILiteDbContext liteDbContext, |
||||
ISettingsManager settingsManager |
||||
) |
||||
{ |
||||
this.logger = logger; |
||||
this.liteDbContext = liteDbContext; |
||||
this.settingsManager = settingsManager; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public async Task<IReadOnlyList<LocalImageFile>> GetLocalImagesByPrefix(string pathPrefix) |
||||
{ |
||||
return await liteDbContext.LocalImageFiles |
||||
.Query() |
||||
.Where(imageFile => imageFile.RelativePath.StartsWith(pathPrefix)) |
||||
.ToArrayAsync() |
||||
.ConfigureAwait(false); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public async Task RefreshIndex(string subPath = "") |
||||
{ |
||||
var imagesDir = settingsManager.ImagesDirectory; |
||||
|
||||
// Start |
||||
var stopwatch = Stopwatch.StartNew(); |
||||
logger.LogInformation("Refreshing images index..."); |
||||
|
||||
using var db = await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false); |
||||
|
||||
var localImageFiles = db.GetCollection<LocalImageFile>("LocalImageFiles")!; |
||||
|
||||
await localImageFiles.DeleteAllAsync().ConfigureAwait(false); |
||||
|
||||
// Record start of actual indexing |
||||
var indexStart = stopwatch.Elapsed; |
||||
|
||||
var added = 0; |
||||
|
||||
foreach ( |
||||
var file in imagesDir.Info |
||||
.EnumerateFiles("*.*", SearchOption.AllDirectories) |
||||
.Where(info => LocalImageFile.SupportedImageExtensions.Contains(info.Extension)) |
||||
.Select(info => new FilePath(info)) |
||||
) |
||||
{ |
||||
var relativePath = Path.GetRelativePath(imagesDir, file); |
||||
|
||||
// Skip if not in sub-path |
||||
if (!string.IsNullOrEmpty(subPath) && !relativePath.StartsWith(subPath)) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
// TODO: Support other types |
||||
const LocalImageFileType imageType = |
||||
LocalImageFileType.Inference | LocalImageFileType.TextToImage; |
||||
|
||||
var localImage = new LocalImageFile |
||||
{ |
||||
RelativePath = relativePath, |
||||
ImageType = imageType |
||||
}; |
||||
|
||||
// Insert into database |
||||
await localImageFiles.InsertAsync(localImage).ConfigureAwait(false); |
||||
|
||||
added++; |
||||
} |
||||
// Record end of actual indexing |
||||
var indexEnd = stopwatch.Elapsed; |
||||
|
||||
await db.CommitAsync().ConfigureAwait(false); |
||||
|
||||
// End |
||||
stopwatch.Stop(); |
||||
var indexDuration = indexEnd - indexStart; |
||||
var dbDuration = stopwatch.Elapsed - indexDuration; |
||||
|
||||
logger.LogInformation( |
||||
"Image index updated for {Prefix} with {Entries} files, took {IndexDuration:F1}ms ({DbDuration:F1}ms db)", |
||||
subPath, |
||||
added, |
||||
indexDuration.TotalMilliseconds, |
||||
dbDuration.TotalMilliseconds |
||||
); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void BackgroundRefreshIndex() |
||||
{ |
||||
RefreshIndex().SafeFireAndForget(); |
||||
} |
||||
} |
Loading…
Reference in new issue