diff --git a/Avalonia.Gif/WebpInstance.cs b/Avalonia.Gif/WebpInstance.cs index b92569349..8ac596f34 100644 --- a/Avalonia.Gif/WebpInstance.cs +++ b/Avalonia.Gif/WebpInstance.cs @@ -54,14 +54,18 @@ public WebpInstance(Stream currentStream) var pixSize = new PixelSize(_codec.Info.Width, _codec.Info.Height); - _targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); + _targetBitmap = new WriteableBitmap( + pixSize, + new Vector(96, 96), + PixelFormat.Bgra8888, + AlphaFormat.Opaque + ); GifPixelSize = pixSize; _totalTime = TimeSpan.Zero; _frameTimes = _codec - .FrameInfo - .Select(frame => + .FrameInfo.Select(frame => { _totalTime = _totalTime.Add(TimeSpan.FromMilliseconds(frame.Duration)); return _totalTime; diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b8fb1ed..4c94d0513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ 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.13.1 +### Changed +- Redesigned the Checkpoint Manager Filter flyout to include more options and improve the layout +- "Clear All" button will now remain at the top of the Downloads list regardless of scroll position - thanks to @Genteure! +- Improved image metadata parsing - thanks to @Genteure! +### Fixed +- Fixed [#1078](https://github.com/LykosAI/StabilityMatrix/issues/1078) - "Call from invalid thread" error after one-click install finishes +- Fixed [#1080](https://github.com/LykosAI/StabilityMatrix/issues/1080) - Some models not displayed in Checkpoint Manager +- Fixed Inference image selector card buttons taking up the whole height of the card +- Fixed Inference mask editor failing to paint to the right-most edge on large images +- Fixed Inference mask editor not showing the entire image in certain circumstances +- Fixed crash when dragging & dropping images in Inference (hopefully) +- Fixed an issue where certain sampler/scheduler combos would not get saved in image metadata - thanks to @yansigit! +### Supporters +#### Visionaries +- A heartfelt thank you to our exceptional Visionary-tier Patreon backers, **Waterclouds** and **TheTekknician**! We truly appreciate your steadfast support! +#### Pioneers +- We are also very grateful to our wonderful Pioneer-tier Patreon supporters, **tankfox**, **Mr Unknown**, **Szir777**, **Tigon**, and **NowFallenAngel**! Your support means a lot to us! + ## v2.13.0 ### Added - Added new package - [ComfyUI-Zluda](https://github.com/patientx/ComfyUI-Zluda) - for AMD GPU users on Windows diff --git a/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml index 0aa53b84c..bf6de24cc 100644 --- a/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml @@ -48,7 +48,7 @@ Source="{Binding ImageSource}" Stretch="Uniform" StretchDirection="Both" /> - + @@ -57,13 +57,13 @@ - + RenderOptions.BitmapInterpolationMode="HighQuality" + Source="{Binding CachedOrNewMaskRenderImage.Bitmap}" + Stretch="Uniform" + StretchDirection="Both" /> @@ -102,16 +102,18 @@ - + IsVisible="{Binding !IsSelectionAvailable}" + Orientation="Horizontal" + Spacing="4"> + + CornerRadius="10" + IsVisible="{Binding IsMaskEditorEnabled}"> + FontSize="{TemplateBinding FontSize}" + IsChecked="{Binding IsMaskOverlayEnabled}"> @@ -142,11 +144,13 @@ Symbol="Eye" /> - + + CornerRadius="10" + IsVisible="{Binding IsMaskEditorEnabled}"> - + diff --git a/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml.cs b/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml.cs index 85cd915e9..3cbbe0e52 100644 --- a/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml.cs @@ -260,7 +260,7 @@ private void HandlePointerMoved(PointerEventArgs e) // penPath.Path.LineTo(cursorPosition.ToSKPoint()); // Get bounds for discarding invalid points - var canvasBounds = MainCanvas?.Bounds ?? new Rect(); + var canvasBounds = new Rect(0, 0, MainCanvas?.Bounds.Width ?? 0, MainCanvas?.Bounds.Height ?? 0); // Add points foreach (var point in points) diff --git a/StabilityMatrix.Avalonia/Models/ImageSource.cs b/StabilityMatrix.Avalonia/Models/ImageSource.cs index df913b042..e0739cfa0 100644 --- a/StabilityMatrix.Avalonia/Models/ImageSource.cs +++ b/StabilityMatrix.Avalonia/Models/ImageSource.cs @@ -277,7 +277,7 @@ protected virtual void Dispose(bool disposing) if (!disposing) return; - Bitmap?.Dispose(); + Bitmap = null; } /// diff --git a/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs b/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs new file mode 100644 index 000000000..9f91ff253 --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs @@ -0,0 +1,92 @@ +using System.Text.Json.Nodes; +using Injectio.Attributes; +using Microsoft.Extensions.Logging; +using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Models.Database; + +namespace StabilityMatrix.Avalonia.Services; + +[RegisterSingleton] +public class CivitBaseModelTypeService( + ILogger logger, + ICivitApi civitApi, + ILiteDbContext dbContext +) : ICivitBaseModelTypeService +{ + private const string CacheId = "BaseModelTypes"; + private static readonly TimeSpan CacheExpiration = TimeSpan.FromHours(24); + + /// + /// Gets the list of base model types, using cache if available and not expired + /// + public async Task> GetBaseModelTypes(bool forceRefresh = false, bool includeAllOption = true) + { + List civitBaseModels = []; + + if (!forceRefresh) + { + var cached = await dbContext.GetCivitBaseModelTypeCacheEntry(CacheId); + if (cached != null && DateTimeOffset.UtcNow.Subtract(cached.CreatedAt) < CacheExpiration) + { + civitBaseModels = cached.ModelTypes; + } + } + + try + { + if (civitBaseModels.Count <= 0) + { + var baseModelsResponse = await civitApi.GetBaseModelList(); + var jsonContent = await baseModelsResponse.Content.ReadAsStringAsync(); + var baseModels = JsonNode.Parse(jsonContent); + + var jArray = + baseModels?["error"]?["issues"]?[0]?["unionErrors"]?[0]?["issues"]?[0]?["options"] + as JsonArray; + + civitBaseModels = jArray?.GetValues().ToList() ?? []; + + // Cache the results + var cacheEntry = new CivitBaseModelTypeCacheEntry + { + Id = CacheId, + ModelTypes = civitBaseModels, + CreatedAt = DateTimeOffset.UtcNow + }; + + await dbContext.UpsertCivitBaseModelTypeCacheEntry(cacheEntry); + } + + if (includeAllOption) + { + civitBaseModels.Insert(0, CivitBaseModelType.All.ToString()); + } + + // Filter and sort results + var filteredResults = civitBaseModels + .Where(s => !s.Equals("odor", StringComparison.OrdinalIgnoreCase)) + .OrderBy(s => s) + .ToList(); + + return filteredResults; + } + catch (Exception e) + { + logger.LogError(e, "Failed to get base model list"); + + // Return cached results if available, even if expired + var expiredCache = await dbContext.GetCivitBaseModelTypeCacheEntry(CacheId); + return expiredCache?.ModelTypes ?? []; + } + } + + /// + /// Clears the cached base model types + /// + public void ClearCache() + { + dbContext.CivitBaseModelTypeCache.DeleteAllAsync(); + } +} diff --git a/StabilityMatrix.Avalonia/Services/ICivitBaseModelTypeService.cs b/StabilityMatrix.Avalonia/Services/ICivitBaseModelTypeService.cs new file mode 100644 index 000000000..9dc58a804 --- /dev/null +++ b/StabilityMatrix.Avalonia/Services/ICivitBaseModelTypeService.cs @@ -0,0 +1,7 @@ +namespace StabilityMatrix.Avalonia.Services; + +public interface ICivitBaseModelTypeService +{ + Task> GetBaseModelTypes(bool forceRefresh = false, bool includeAllOption = true); + void ClearCache(); +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index 01be71b4c..0e3a985f1 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -421,7 +421,7 @@ await notificationService.ShowAsync( // Clear progress OutputProgress.ClearProgress(); - ImageGalleryCardViewModel.PreviewImage?.Dispose(); + // ImageGalleryCardViewModel.PreviewImage?.Dispose(); ImageGalleryCardViewModel.PreviewImage = null; ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false; diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/PageViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/PageViewModelBase.cs index c1261a8ce..3d1da920b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/PageViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/PageViewModelBase.cs @@ -5,7 +5,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Base; /// /// An abstract class for enabling page navigation. /// -public abstract class PageViewModelBase : ViewModelBase +public abstract class PageViewModelBase : DisposableViewModelBase { /// /// Gets if the user can navigate to the next page @@ -16,7 +16,7 @@ public abstract class PageViewModelBase : ViewModelBase /// Gets if the user can navigate to the previous page /// public virtual bool CanNavigatePrevious { get; protected set; } - + public abstract string Title { get; } public abstract IconSource IconSource { get; } } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs index 3f11591a0..309accaa2 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs @@ -45,6 +45,7 @@ public sealed partial class CivitAiBrowserViewModel : TabViewModelBase, IInfinit private readonly ISettingsManager settingsManager; private readonly ILiteDbContext liteDbContext; private readonly INotificationService notificationService; + private readonly ICivitBaseModelTypeService baseModelTypeService; private bool dontSearch = false; private readonly SourceCache, int> modelCache = new(static ov => ov.Value.Id); @@ -126,13 +127,15 @@ public CivitAiBrowserViewModel( ISettingsManager settingsManager, ServiceManager dialogFactory, ILiteDbContext liteDbContext, - INotificationService notificationService + INotificationService notificationService, + ICivitBaseModelTypeService baseModelTypeService ) { this.civitApi = civitApi; this.settingsManager = settingsManager; this.liteDbContext = liteDbContext; this.notificationService = notificationService; + this.baseModelTypeService = baseModelTypeService; EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; @@ -727,31 +730,7 @@ private void UpdateResultsText() [Localizable(false)] private async Task> GetBaseModelList() { - try - { - var baseModelsResponse = await civitApi.GetBaseModelList(); - var jsonContent = await baseModelsResponse.Content.ReadAsStringAsync(); - var baseModels = JsonNode.Parse(jsonContent); - - var jArray = - baseModels?["error"]?["issues"]?[0]?["unionErrors"]?[0]?["issues"]?[0]?["options"] - as JsonArray; - var civitBaseModels = jArray?.GetValues().ToList() ?? []; - - civitBaseModels.Insert(0, CivitBaseModelType.All.ToString()); - - var filteredResults = civitBaseModels - .Where(s => s.Equals("odor", StringComparison.OrdinalIgnoreCase) == false) - .OrderBy(s => s) - .ToList(); - - return filteredResults; - } - catch (Exception e) - { - Logger.Error(e, "Failed to get base model list"); - return []; - } + return await baseModelTypeService.GetBaseModelTypes(); } public override string Header => Resources.Label_CivitAi; diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs index 360b73099..057d5ba83 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs @@ -59,7 +59,8 @@ public partial class CheckpointsPageViewModel( INotificationService notificationService, IMetadataImportService metadataImportService, IModelImportService modelImportService, - ServiceManager dialogFactory + ServiceManager dialogFactory, + ICivitBaseModelTypeService baseModelTypeService ) : PageViewModelBase { public override string Title => Resources.Label_CheckpointManager; @@ -116,14 +117,6 @@ ServiceManager dialogFactory [ObservableProperty] private bool isDragOver; - [ObservableProperty] - private ObservableCollection baseModelOptions = - new( - Enum.GetValues() - .Where(x => x != CivitBaseModelType.All) - .Select(x => x.GetStringValue()) - ); - [ObservableProperty] private ObservableCollection selectedBaseModels = []; @@ -156,26 +149,65 @@ ServiceManager dialogFactory ? Resources.Label_OneImageSelected.Replace("images ", "") : string.Format(Resources.Label_NumImagesSelected, NumItemsSelected).Replace("images ", ""); - protected override void OnInitialLoaded() + private SourceCache BaseModelCache { get; } = new(s => s); + + public IObservableCollection BaseModelOptions { get; set; } = + new ObservableCollectionExtended(); + + protected override async Task OnInitialLoadedAsync() { if (Design.IsDesignMode) return; - base.OnInitialLoaded(); + await base.OnInitialLoadedAsync(); + + var settingsSelectedBaseModels = settingsManager.Settings.SelectedBaseModels; + + AddDisposable( + BaseModelCache + .Connect() + .DeferUntilLoaded() + .Transform( + baseModel => + new BaseModelOptionViewModel + { + ModelType = baseModel, + IsSelected = settingsSelectedBaseModels.Contains(baseModel) + } + ) + .Bind(BaseModelOptions) + .WhenPropertyChanged(p => p.IsSelected) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(next => + { + if (next.Sender.IsSelected) + SelectedBaseModels.Add(next.Sender.ModelType); + else + SelectedBaseModels.Remove(next.Sender.ModelType); + + OnPropertyChanged(nameof(ClearButtonText)); + OnPropertyChanged(nameof(SelectedBaseModels)); + }) + ); - SelectedBaseModels = new ObservableCollection(BaseModelOptions); - SelectedBaseModels.CollectionChanged += (_, _) => - { - OnPropertyChanged(nameof(ClearButtonText)); - OnPropertyChanged(nameof(SelectedBaseModels)); - settingsManager.Transaction( - settings => settings.SelectedBaseModels = SelectedBaseModels.ToList() - ); - }; + var settingsTransactionObservable = this.WhenPropertyChanged(x => x.SelectedBaseModels) + .Throttle(TimeSpan.FromMilliseconds(50)) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(_ => + { + settingsManager.Transaction( + settings => settings.SelectedBaseModels = SelectedBaseModels.ToList() + ); + }); + + AddDisposable(settingsTransactionObservable); + + var baseModelTypes = await baseModelTypeService.GetBaseModelTypes(includeAllOption: false); + BaseModelCache.EditDiff(baseModelTypes); // Observable predicate from SearchQuery changes var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) - .Throttle(TimeSpan.FromMilliseconds(100))! + .Throttle(TimeSpan.FromMilliseconds(100)) .Select( _ => (Func)( @@ -228,9 +260,7 @@ or nameof(SortConnectedModelsFirst) { var comparer = new SortExpressionComparer(); if (SortConnectedModelsFirst) - { comparer = comparer.ThenByDescending(vm => vm.CheckpointFile.HasConnectedModel); - } switch (SelectedSortOption) { @@ -301,31 +331,33 @@ or nameof(SortConnectedModelsFirst) .ObserveOn(SynchronizationContext.Current) .AsObservable(); - ModelsCache - .Connect() - .DeferUntilLoaded() - .Filter(filterPredicate) - .Filter(searchPredicate) - .Transform( - x => - new CheckpointFileViewModel( - settingsManager, - modelIndexService, - notificationService, - downloadService, - dialogFactory, - logger, - x - ) - ) - .SortAndBind(Models, comparerObservable) - .WhenPropertyChanged(p => p.IsSelected) - .Throttle(TimeSpan.FromMilliseconds(50)) - .ObserveOn(SynchronizationContext.Current) - .Subscribe(_ => - { - NumItemsSelected = Models.Count(o => o.IsSelected); - }); + AddDisposable( + ModelsCache + .Connect() + .DeferUntilLoaded() + .Filter(filterPredicate) + .Filter(searchPredicate) + .Transform( + x => + new CheckpointFileViewModel( + settingsManager, + modelIndexService, + notificationService, + downloadService, + dialogFactory, + logger, + x + ) + ) + .SortAndBind(Models, comparerObservable) + .WhenPropertyChanged(p => p.IsSelected) + .Throttle(TimeSpan.FromMilliseconds(50)) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(_ => + { + NumItemsSelected = Models.Count(o => o.IsSelected); + }) + ); var categoryFilterPredicate = Observable .FromEventPattern(this, nameof(PropertyChanged)) @@ -336,31 +368,37 @@ or nameof(SortConnectedModelsFirst) .ObserveOn(SynchronizationContext.Current) .AsObservable(); - categoriesCache - .Connect() - .DeferUntilLoaded() - .Filter(categoryFilterPredicate) - .SortAndBind( - Categories, - SortExpressionComparer - .Descending(x => x.Name == "All Models") - .ThenByAscending(x => x.Name) - ) - .ObserveOn(SynchronizationContext.Current) - .Subscribe(); + AddDisposable( + categoriesCache + .Connect() + .DeferUntilLoaded() + .Filter(categoryFilterPredicate) + .SortAndBind( + Categories, + SortExpressionComparer + .Descending(x => x.Name == "All Models") + .ThenByAscending(x => x.Name) + ) + .ObserveOn(SynchronizationContext.Current) + .Subscribe() + ); - settingsManager.RelayPropertyFor( - this, - vm => vm.IsImportAsConnectedEnabled, - s => s.IsImportAsConnected, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.IsImportAsConnectedEnabled, + s => s.IsImportAsConnected, + true + ) ); - settingsManager.RelayPropertyFor( - this, - vm => vm.ResizeFactor, - s => s.CheckpointsPageResizeFactor, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.ResizeFactor, + s => s.CheckpointsPageResizeFactor, + true + ) ); Refresh().SafeFireAndForget(); @@ -374,53 +412,67 @@ or nameof(SortConnectedModelsFirst) ); }; - settingsManager.RelayPropertyFor( - this, - vm => vm.SortConnectedModelsFirst, - settings => settings.SortConnectedModelsFirst, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.SortConnectedModelsFirst, + settings => settings.SortConnectedModelsFirst, + true + ) ); - settingsManager.RelayPropertyFor( - this, - vm => vm.SelectedSortOption, - settings => settings.CheckpointSortMode, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.SelectedSortOption, + settings => settings.CheckpointSortMode, + true + ) ); - settingsManager.RelayPropertyFor( - this, - vm => vm.SelectedSortDirection, - settings => settings.CheckpointSortDirection, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.SelectedSortDirection, + settings => settings.CheckpointSortDirection, + true + ) ); - settingsManager.RelayPropertyFor( - this, - vm => vm.ShowModelsInSubfolders, - settings => settings.ShowModelsInSubfolders, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.ShowModelsInSubfolders, + settings => settings.ShowModelsInSubfolders, + true + ) ); - settingsManager.RelayPropertyFor( - this, - vm => vm.DragMovesAllSelected, - settings => settings.DragMovesAllSelected, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.DragMovesAllSelected, + settings => settings.DragMovesAllSelected, + true + ) ); - settingsManager.RelayPropertyFor( - this, - vm => vm.HideEmptyRootCategories, - settings => settings.HideEmptyRootCategories, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.HideEmptyRootCategories, + settings => settings.HideEmptyRootCategories, + true + ) ); - settingsManager.RelayPropertyFor( - this, - vm => vm.ShowNsfwImages, - settings => settings.ShowNsfwInCheckpointsPage, - true + AddDisposable( + settingsManager.RelayPropertyFor( + this, + vm => vm.ShowNsfwImages, + settings => settings.ShowNsfwInCheckpointsPage, + true + ) ); // make sure a sort happens @@ -444,9 +496,7 @@ private void ClearSelection() { var selected = Models.Where(x => x.IsSelected).ToList(); foreach (var model in selected) - { model.IsSelected = false; - } NumItemsSelected = 0; } @@ -459,9 +509,7 @@ private async Task DeleteAsync() || Models.Where(o => o.IsSelected).Select(vm => vm.CheckpointFile).ToList() is not { Count: > 0 } selectedModelFiles ) - { return; - } var pathsToDelete = selectedModelFiles .SelectMany(x => x.GetDeleteFullPaths(settingsManager.ModelsDirectory)) @@ -471,9 +519,7 @@ private async Task DeleteAsync() confirmDeleteVm.PathsToDelete = pathsToDelete; if (await confirmDeleteVm.GetDialog().ShowAsync() != ContentDialogResult.Primary) - { return; - } try { @@ -534,17 +580,11 @@ private Task OnItemClick(CheckpointFileViewModel item) { // Select item if we're in "select mode" if (NumItemsSelected > 0) - { item.IsSelected = !item.IsSelected; - } else if (item.CheckpointFile.HasConnectedModel) - { return ShowVersionDialog(item); - } else - { item.IsSelected = !item.IsSelected; - } return Task.CompletedTask; } @@ -596,7 +636,7 @@ private async Task ShowVersionDialog(CheckpointFileViewModel item) IsFooterVisible = false, CloseOnClickOutside = true, MaxDialogWidth = 750, - MaxDialogHeight = 1000, + MaxDialogHeight = 1000 }; var htmlDescription = $"""{model.Description}"""; @@ -649,14 +689,9 @@ private async Task ShowVersionDialog(CheckpointFileViewModel item) private void ClearOrSelectAllBaseModels() { if (SelectedBaseModels.Count == BaseModelOptions.Count) - { - SelectedBaseModels.Clear(); - } + BaseModelOptions.ForEach(x => x.IsSelected = false); else - { - SelectedBaseModels.Clear(); - SelectedBaseModels.AddRange(BaseModelOptions); - } + BaseModelOptions.ForEach(x => x.IsSelected = true); } [RelayCommand] @@ -701,10 +736,12 @@ await notificationService.TryAsync( } [RelayCommand] - private Task OpenFolderFromTreeview(object? treeViewItem) => - treeViewItem is CheckpointCategory category && !string.IsNullOrWhiteSpace(category.Path) + private Task OpenFolderFromTreeview(object? treeViewItem) + { + return treeViewItem is CheckpointCategory category && !string.IsNullOrWhiteSpace(category.Path) ? ProcessRunner.OpenFolderBrowser(category.Path) : Task.CompletedTask; + } [RelayCommand] private async Task DeleteFolderAsync(object? treeViewItem) @@ -826,19 +863,13 @@ public async Task MoveBetweenFolders(LocalModelFile sourceFile, DirectoryPath de // Move files if (File.Exists(sourcePath)) - { await FileTransfers.MoveFileAsync(sourcePath, destinationFilePath); - } if (File.Exists(sourceCmInfoPath)) - { await FileTransfers.MoveFileAsync(sourceCmInfoPath, destinationCmInfoPath); - } if (File.Exists(sourcePreviewPath)) - { await FileTransfers.MoveFileAsync(sourcePreviewPath, destinationPreviewPath); - } notificationService.Show( "Model moved successfully", @@ -892,7 +923,6 @@ private void RefreshCategories() .ToList(); foreach (var checkpointCategory in modelCategories.SelectMany(c => c.Flatten())) - { checkpointCategory.Count = Directory .EnumerateFileSystemEntries( checkpointCategory.Path, @@ -900,13 +930,12 @@ private void RefreshCategories() EnumerationOptionConstants.AllDirectories ) .Count(x => LocalModelFile.SupportedCheckpointExtensions.Contains(Path.GetExtension(x))); - } var rootCategory = new CheckpointCategory { Path = settingsManager.ModelsDirectory, Name = "All Models", - Count = modelIndexService.ModelIndex.Values.SelectMany(x => x).Count(), + Count = modelIndexService.ModelIndex.Values.SelectMany(x => x).Count() }; categoriesCache.Edit(updater => @@ -928,9 +957,7 @@ private void RefreshCategories() .SelectMany(x => x.Flatten()) .FirstOrDefault(x => x.Path == dirPath.FullPath); if (category != null) - { category.IsExpanded = true; - } dirPath = dirPath.Parent; } @@ -966,13 +993,11 @@ private ObservableCollection GetSubfolders(string strPath) Path = dir, Count = new DirectoryInfo(dir) .EnumerateFileSystemInfos("*", EnumerationOptionConstants.AllDirectories) - .Count(x => LocalModelFile.SupportedCheckpointExtensions.Contains(x.Extension)), + .Count(x => LocalModelFile.SupportedCheckpointExtensions.Contains(x.Extension)) }; if (Directory.GetDirectories(dir, "*", EnumerationOptionConstants.TopLevelOnly).Length > 0) - { category.SubDirectories = GetSubfolders(dir); - } subfolders.Add(category); } @@ -983,11 +1008,9 @@ private ObservableCollection GetSubfolders(string strPath) private string GetConnectedModelInfoFilePath(string filePath) { if (string.IsNullOrEmpty(filePath)) - { throw new InvalidOperationException( "Cannot get connected model info file path when filePath is empty" ); - } var modelNameNoExt = Path.GetFileNameWithoutExtension((string?)filePath); var modelDir = Path.GetDirectoryName((string?)filePath) ?? ""; @@ -1017,11 +1040,9 @@ private void DelayedClearViewModelProgress(CheckpointFileViewModel viewModel, Ti private bool FilterModels(LocalModelFile file) { if (SelectedCategory?.Path is null || SelectedCategory?.Path == settingsManager.ModelsDirectory) - { return file.HasConnectedModel ? SelectedBaseModels.Contains(file.ConnectedModelInfo.BaseModel ?? "Other") : SelectedBaseModels.Contains("Other"); - } var folderPath = Path.GetDirectoryName(file.RelativePath); var categoryRelativePath = SelectedCategory @@ -1030,9 +1051,7 @@ private bool FilterModels(LocalModelFile file) .TrimStart(Path.DirectorySeparatorChar); if (categoryRelativePath == null || folderPath == null) - { return false; - } if ( ( @@ -1042,9 +1061,7 @@ private bool FilterModels(LocalModelFile file) ) is false ) - { return false; - } return ShowModelsInSubfolders ? folderPath.StartsWith(categoryRelativePath) diff --git a/StabilityMatrix.Avalonia/ViewModels/Controls/PaintCanvasViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Controls/PaintCanvasViewModel.cs index d8b3937c7..cbe9e130d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Controls/PaintCanvasViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Controls/PaintCanvasViewModel.cs @@ -215,7 +215,22 @@ public void RenderToSurface( } else { - layer.Surface.Canvas.Clear(SKColors.Transparent); + var currentInfo = layer.Surface.Canvas.DeviceClipBounds; + if (currentInfo.Width != CanvasSize.Width || currentInfo.Height != CanvasSize.Height) + { + // Dispose the old surface + layer.Surface.Dispose(); + + // Create a brand-new SKSurface with the new size + layer.Surface = SKSurface.Create( + new SKImageInfo(CanvasSize.Width, CanvasSize.Height) + ); + } + else + { + // No resize needed, just clear + layer.Surface.Canvas.Clear(SKColors.Transparent); + } } } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs index 9f31c3500..623d43db8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs @@ -87,146 +87,138 @@ INotificationService notificationService .ThenByAscending(p => p.DisplayName) ) .ObserveOn(SynchronizationContext.Current) - .Subscribe(); + .Subscribe(_ => + { + if (ShownPackages.Count > 0) + return; - AllPackagesCache.AddOrUpdate(packageFactory.GetAllAvailablePackages()); - if (ShownPackages.Count > 0) - return; + ShowIncompatiblePackages = true; + }); - ShowIncompatiblePackages = true; + AllPackagesCache.AddOrUpdate(packageFactory.GetAllAvailablePackages()); } [RelayCommand] - private void InstallComfyForInference() + private async Task InstallComfyForInference() { var comfyPackage = ShownPackages.FirstOrDefault(x => x is ComfyUI); if (comfyPackage == null) return; isInferenceInstall = true; - InstallPackage(comfyPackage); + await InstallPackage(comfyPackage); } [RelayCommand] - private void InstallPackage(BasePackage selectedPackage) + private async Task InstallPackage(BasePackage selectedPackage) { - Task.Run(async () => - { - var installLocation = Path.Combine( - settingsManager.LibraryDir, - "Packages", - selectedPackage.Name - ); - - var steps = new List - { - new SetPackageInstallingStep(settingsManager, selectedPackage.Name), - new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, selectedPackage), - }; - - // get latest version & download & install - if (Directory.Exists(installLocation)) - { - var installPath = new DirectoryPath(installLocation); - await installPath.DeleteVerboseAsync(logger); - } - - var downloadVersion = await selectedPackage.GetLatestVersion(); - var installedVersion = new InstalledPackageVersion { IsPrerelease = false }; + OnPrimaryButtonClick(); - if (selectedPackage.ShouldIgnoreReleases) - { - installedVersion.InstalledBranch = downloadVersion.BranchName; - installedVersion.InstalledCommitSha = downloadVersion.CommitHash; - } - else - { - installedVersion.InstalledReleaseVersion = downloadVersion.VersionTag; - } + var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", selectedPackage.Name); + + var steps = new List + { + new SetPackageInstallingStep(settingsManager, selectedPackage.Name), + new SetupPrerequisitesStep(prerequisiteHelper, pyRunner, selectedPackage) + }; + + // get latest version & download & install + if (Directory.Exists(installLocation)) + { + var installPath = new DirectoryPath(installLocation); + await installPath.DeleteVerboseAsync(logger); + } + + var downloadVersion = await selectedPackage.GetLatestVersion(); + var installedVersion = new InstalledPackageVersion { IsPrerelease = false }; + + if (selectedPackage.ShouldIgnoreReleases) + { + installedVersion.InstalledBranch = downloadVersion.BranchName; + installedVersion.InstalledCommitSha = downloadVersion.CommitHash; + } + else + { + installedVersion.InstalledReleaseVersion = downloadVersion.VersionTag; + } + + var torchVersion = selectedPackage.GetRecommendedTorchVersion(); + var recommendedSharedFolderMethod = selectedPackage.RecommendedSharedFolderMethod; + + var installedPackage = new InstalledPackage + { + DisplayName = selectedPackage.DisplayName, + LibraryPath = Path.Combine("Packages", selectedPackage.Name), + Id = Guid.NewGuid(), + PackageName = selectedPackage.Name, + Version = installedVersion, + LaunchCommand = selectedPackage.LaunchCommand, + LastUpdateCheck = DateTimeOffset.Now, + PreferredTorchIndex = torchVersion, + PreferredSharedFolderMethod = recommendedSharedFolderMethod + }; + + var downloadStep = new DownloadPackageVersionStep( + selectedPackage, + installLocation, + new DownloadPackageOptions { VersionOptions = downloadVersion } + ); + steps.Add(downloadStep); + + var unpackSiteCustomizeStep = new UnpackSiteCustomizeStep(Path.Combine(installLocation, "venv")); + steps.Add(unpackSiteCustomizeStep); + + var installStep = new InstallPackageStep( + selectedPackage, + installLocation, + installedPackage, + new InstallPackageOptions + { + SharedFolderMethod = recommendedSharedFolderMethod, + VersionOptions = downloadVersion, + PythonOptions = { TorchIndex = torchVersion } + } + ); + steps.Add(installStep); + + var setupModelFoldersStep = new SetupModelFoldersStep( + selectedPackage, + recommendedSharedFolderMethod, + installLocation + ); + steps.Add(setupModelFoldersStep); + + var setupOutputSharingStep = new SetupOutputSharingStep(selectedPackage, installLocation); + steps.Add(setupOutputSharingStep); + + var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, installedPackage); + steps.Add(addInstalledPackageStep); + + var runner = new PackageModificationRunner + { + ShowDialogOnStart = false, + HideCloseButton = false, + ModificationCompleteMessage = $"{selectedPackage.DisplayName} installed successfully" + }; + + runner + .ExecuteSteps(steps) + .ContinueWith(_ => + { + notificationService.OnPackageInstallCompleted(runner); - var torchVersion = selectedPackage.GetRecommendedTorchVersion(); - var recommendedSharedFolderMethod = selectedPackage.RecommendedSharedFolderMethod; + EventManager.Instance.OnOneClickInstallFinished(false); - var installedPackage = new InstalledPackage - { - DisplayName = selectedPackage.DisplayName, - LibraryPath = Path.Combine("Packages", selectedPackage.Name), - Id = Guid.NewGuid(), - PackageName = selectedPackage.Name, - Version = installedVersion, - LaunchCommand = selectedPackage.LaunchCommand, - LastUpdateCheck = DateTimeOffset.Now, - PreferredTorchIndex = torchVersion, - PreferredSharedFolderMethod = recommendedSharedFolderMethod - }; - - var downloadStep = new DownloadPackageVersionStep( - selectedPackage, - installLocation, - new DownloadPackageOptions { VersionOptions = downloadVersion } - ); - steps.Add(downloadStep); - - var unpackSiteCustomizeStep = new UnpackSiteCustomizeStep( - Path.Combine(installLocation, "venv") - ); - steps.Add(unpackSiteCustomizeStep); - - var installStep = new InstallPackageStep( - selectedPackage, - installLocation, - installedPackage, - new InstallPackageOptions - { - SharedFolderMethod = recommendedSharedFolderMethod, - VersionOptions = downloadVersion, - PythonOptions = { TorchIndex = torchVersion } - } - ); - steps.Add(installStep); - - var setupModelFoldersStep = new SetupModelFoldersStep( - selectedPackage, - recommendedSharedFolderMethod, - installLocation - ); - steps.Add(setupModelFoldersStep); - - var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, installedPackage); - steps.Add(addInstalledPackageStep); + if (!isInferenceInstall) + return; Dispatcher.UIThread.Post(() => { - var runner = new PackageModificationRunner - { - ShowDialogOnStart = false, - HideCloseButton = false, - ModificationCompleteMessage = $"{selectedPackage.DisplayName} installed successfully" - }; - - runner - .ExecuteSteps(steps) - .ContinueWith(_ => - { - notificationService.OnPackageInstallCompleted(runner); - - EventManager.Instance.OnOneClickInstallFinished(false); - - if (!isInferenceInstall) - return; - - Dispatcher.UIThread.Post(() => - { - navigationService.NavigateTo(); - }); - }) - .SafeFireAndForget(); - - EventManager.Instance.OnPackageInstallProgressAdded(runner); + navigationService.NavigateTo(); }); }) .SafeFireAndForget(); - OnPrimaryButtonClick(); + EventManager.Instance.OnPackageInstallProgressAdded(runner); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs index a68a82812..05f208152 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs @@ -115,7 +115,7 @@ public override async Task OnLoadedAsync() ) ); } - catch (ApiException e) + catch (Exception e) { // hide dialog and show error msg logger.LogError(e, "Failed to get recommended models"); diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs index 04189239a..8852a5e51 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs @@ -88,7 +88,7 @@ public void SetPreviewImage(byte[] imageBytes) PreviewImage = bitmap; IsPreviewOverlayEnabled = true; - currentImage?.Dispose(); + // currentImage?.Dispose(); }); } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs index 5e7233f29..b77fdbfd6 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs @@ -7,6 +7,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using AsyncAwaitBestPractices; +using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Platform.Storage; using Avalonia.Threading; @@ -206,6 +207,11 @@ private async Task OpenEditMaskDialogAsync() if (await ImageSource.GetBitmapAsync() is not { } currentBitmap) { Logger.Warn("GetBitmapAsync returned null for image {Path}", ImageSource.LocalFile?.FullPath); + notificationService.ShowPersistent( + "Error Loading Image", + "Could not load mask editor for the provided image.", + NotificationType.Error + ); return; } MaskEditorViewModel.PaintCanvasViewModel.BackgroundImage = currentBitmap.ToSKBitmap(); @@ -289,6 +295,6 @@ private void LoadUserImage(ImageSource image) ImageSource = image; - current?.Dispose(); + // current?.Dispose(); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/MainPackageManagerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/MainPackageManagerViewModel.cs index 67b98c1d6..c701142bf 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/MainPackageManagerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/MainPackageManagerViewModel.cs @@ -114,7 +114,7 @@ RunningPackageService runningPackageService private void OnOneClickInstallFinished(object? sender, bool e) { - OnLoadedAsync().SafeFireAndForget(); + Dispatcher.UIThread.Post(() => OnLoadedAsync().SafeFireAndForget()); } public void SetPackages(IEnumerable packages) diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index 709ffaf36..82b60b948 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -34,6 +34,7 @@ False True + @@ -288,28 +289,27 @@ - +