diff --git a/CHANGELOG.md b/CHANGELOG.md index e67cfbe3..066bea76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ 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.3.3 +### Fixed +- Fixed GPU recognition for Nvidia Tesla GPUs +- Fix checkpoint file index extension identification with some path names +- Fixed issue where config file may be overwritten during Automatic1111 package updates +- Fixed "Directory Not Found" error on startup when previously selected Data directory does not exist + +## v2.3.2 +### Added +- Added warning for exFAT / FAT32 drives when selecting a data directory +### Fixed +- Automatic1111 and ComfyUI should now install the correct version of pytorch for AMD GPUs +- Fixed "Call from invalid thread" exceptions preventing download completion notifications from showing +- Fixed model preview image downloading with incorrect name +### Changed +- Redesigned "Select Model Version" dialog to include model description and all preview images + ## v2.3.1 ### Fixed - Fixed Auto update not appearing in some regions due to date formatting issues diff --git a/StabilityMatrix.Avalonia/Assets.cs b/StabilityMatrix.Avalonia/Assets.cs index bcbfe733..b9c9a6c8 100644 --- a/StabilityMatrix.Avalonia/Assets.cs +++ b/StabilityMatrix.Avalonia/Assets.cs @@ -33,17 +33,22 @@ internal static class Assets [SupportedOSPlatform("windows")] [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] public static AvaloniaResource SevenZipExecutable => Compat.Switch( (PlatformKind.Windows, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")), (PlatformKind.Linux | PlatformKind.X64, - new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", (UnixFileMode) 0x777))); + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", (UnixFileMode) 0777)), + (PlatformKind.MacOS | PlatformKind.Arm, + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", (UnixFileMode) 0x777))); [SupportedOSPlatform("windows")] [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] public static AvaloniaResource SevenZipLicense => Compat.Switch( (PlatformKind.Windows, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt")), - (PlatformKind.Linux | PlatformKind.X64, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt"))); + (PlatformKind.Linux | PlatformKind.X64, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt")), + (PlatformKind.MacOS | PlatformKind.Arm, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt"))); public static AvaloniaResource PyScriptSiteCustomize => new( "avares://StabilityMatrix.Avalonia/Assets/sitecustomize.py"); diff --git a/StabilityMatrix.Avalonia/Assets/macos-arm64/7zz b/StabilityMatrix.Avalonia/Assets/macos-arm64/7zz new file mode 100755 index 00000000..a7ea6fde Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/macos-arm64/7zz differ diff --git a/StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt b/StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt new file mode 100644 index 00000000..8650d994 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt @@ -0,0 +1,88 @@ + 7-Zip + ~~~~~ + License for use and distribution + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + 7-Zip Copyright (C) 1999-2023 Igor Pavlov. + + The licenses for 7zz and 7zzs files are: + + - The "GNU LGPL" as main license for most of the code + - The "GNU LGPL" with "unRAR license restriction" for some code + - The "BSD 3-clause License" for some code + + Redistributions in binary form must reproduce related license information from this file. + + Note: + You can use 7-Zip on any computer, including a computer in a commercial + organization. You don't need to register or pay for 7-Zip. + + + GNU LGPL information + -------------------- + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You can receive a copy of the GNU Lesser General Public License from + http://www.gnu.org/ + + + + + BSD 3-clause License + -------------------- + + The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression. + That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, + that also uses the "BSD 3-clause License": + + ---- + Copyright (c) 2015-2016, Apple Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ---- + + + + + unRAR license restriction + ------------------------- + + The decompression engine for RAR archives was developed using source + code of unRAR program. + All copyrights to original unRAR code are owned by Alexander Roshal. + + The license for original unRAR code has the following restriction: + + The unRAR sources cannot be used to re-create the RAR compression algorithm, + which is proprietary. Distribution of modified unRAR sources in separate form + or as a part of other software is permitted, provided that it is clearly + stated in the documentation and source comments that the code may + not be used to develop a RAR (WinRAR) compatible archiver. + + + -- + Igor Pavlov diff --git a/StabilityMatrix.Avalonia/Controls/BetterImage.cs b/StabilityMatrix.Avalonia/Controls/BetterImage.cs index 8eb77eee..09405ca7 100644 --- a/StabilityMatrix.Avalonia/Controls/BetterImage.cs +++ b/StabilityMatrix.Avalonia/Controls/BetterImage.cs @@ -1,8 +1,10 @@ -using Avalonia; +using System; +using Avalonia; using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Automation.Peers; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Metadata; @@ -77,25 +79,48 @@ public class BetterImage : Control { var source = Source; - if (source != null && Bounds.Width > 0 && Bounds.Height > 0) + if (source == null || Bounds is not {Width: > 0, Height: > 0}) return; + + var viewPort = new Rect(Bounds.Size); + var sourceSize = source.Size; + + var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); + var scaledSize = sourceSize * scale; + + // Calculate starting points for dest + var destX = HorizontalAlignment switch { - Rect viewPort = new Rect(Bounds.Size); - Size sourceSize = source.Size; - - Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); - Size scaledSize = sourceSize * scale; - Rect destRect = viewPort - .CenterRect(new Rect(scaledSize)) - .WithX(0) - .WithY(0) - .Intersect(viewPort); - Rect sourceRect = new Rect(sourceSize) - .CenterRect(new Rect(destRect.Size / scale)) - .WithX(0) - .WithY(0); - - context.DrawImage(source, sourceRect, destRect); - } + HorizontalAlignment.Left => 0, + HorizontalAlignment.Center => (int) (viewPort.Width - scaledSize.Width) / 2, + HorizontalAlignment.Right => (int) (viewPort.Width - scaledSize.Width), + // Stretch is default, use center + HorizontalAlignment.Stretch => (int) (viewPort.Width - scaledSize.Width) / 2, + _ => throw new ArgumentException(nameof(HorizontalAlignment)) + }; + + var destRect = viewPort + .CenterRect(new Rect(scaledSize)) + .WithX(destX) + .WithY(0) + .Intersect(viewPort); + var destRectUnscaledSize = destRect.Size / scale; + + var sourceX = HorizontalAlignment switch + { + HorizontalAlignment.Left => 0, + HorizontalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Width / 2, + HorizontalAlignment.Right => (int) (sourceSize - destRectUnscaledSize).Width, + // Stretch is default, use center + HorizontalAlignment.Stretch => (int) (sourceSize - destRectUnscaledSize).Width / 2, + _ => throw new ArgumentException(nameof(HorizontalAlignment)) + }; + + var sourceRect = new Rect(sourceSize) + .CenterRect(new Rect(destRect.Size / scale)) + .WithX(sourceX) + .WithY(0); + + context.DrawImage(source, sourceRect, destRect); } /// diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index f26c8c58..9d07d038 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -340,7 +340,9 @@ public static class DesignData new() { Name = "BB95 Furry Mix", - Description = "v1.0.0", + Description = @"Introducing SnoutMix +A Mix of non-Furry and Furry models such as Furtastic and BB95Furry to create a great variety of anthro AI generation options, but bringing out more detail, still giving a lot of freedom to customise the human aspects, and having great backgrounds, with a focus on something more realistic. Works well with realistic character loras. +The gallery images are often inpainted, but you will get something very similar if copying their data directly. They are inpainted using the same model, therefore all results are possible without anything custom/hidden-away. Controlnet Tiled is applied to enhance them further afterwards. Gallery images were made with same model but before it was renamed", BaseModel = "SD 1.5", Files = new List { @@ -378,6 +380,8 @@ public static class DesignData // Sample data for dialogs vm.Versions = new List {sampleViewModel}; + vm.Title = sampleCivitVersions[0].Name; + vm.Description = sampleCivitVersions[0].Description; vm.SelectedVersionViewModel = sampleViewModel; }); diff --git a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs index 8e72b130..638333c0 100644 --- a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs @@ -173,8 +173,8 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper progress?.Report(new ProgressReport(0, "Installing Python", isIndeterminate: true)); await ArchiveHelper.Extract7ZAuto(downloadPath, PythonDir); - // For Linux, move the inner 'python' folder up to the root PythonDir - if (Compat.IsLinux) + // For Unix, move the inner 'python' folder up to the root PythonDir + if (Compat.IsUnix) { var innerPythonDir = PythonDir.JoinDir("python"); if (!innerPythonDir.Exists) diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 288e28f2..c2a6c865 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -24,6 +24,7 @@ + @@ -89,6 +90,8 @@ + + diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs index e9b2bd86..6905cc07 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Net.Http; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Notifications; @@ -13,21 +12,18 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using NLog; -using Octokit; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Extensions; -using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; -using Notification = Avalonia.Controls.Notifications.Notification; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; @@ -39,7 +35,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel private readonly ISettingsManager settingsManager; private readonly ServiceManager dialogFactory; private readonly INotificationService notificationService; - + public Action? OnDownloadStart { get; set; } public CivitModel CivitModel @@ -54,11 +50,18 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel } public override bool IsTextVisible => Value > 0; - - [ObservableProperty] private Uri? cardImage; - [ObservableProperty] private bool isImporting; - [ObservableProperty] private string updateCardText = string.Empty; - [ObservableProperty] private bool showUpdateCard; + + [ObservableProperty] + private Uri? cardImage; + + [ObservableProperty] + private bool isImporting; + + [ObservableProperty] + private string updateCardText = string.Empty; + + [ObservableProperty] + private bool showUpdateCard; private CivitModel civitModel; public CheckpointBrowserCardViewModel( @@ -66,7 +69,8 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel ITrackedDownloadService trackedDownloadService, ISettingsManager settingsManager, ServiceManager dialogFactory, - INotificationService notificationService) + INotificationService notificationService + ) { this.downloadService = downloadService; this.trackedDownloadService = trackedDownloadService; @@ -77,7 +81,8 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel // Update image when nsfw setting changes settingsManager.RegisterPropertyChangedHandler( s => s.ModelBrowserNsfwEnabled, - _ => Dispatcher.UIThread.Post(UpdateImage)); + _ => Dispatcher.UIThread.Post(UpdateImage) + ); } private void CheckIfInstalled() @@ -88,28 +93,45 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel ShowUpdateCard = true; return; } - - if (CivitModel.ModelVersions == null) return; - + + if (CivitModel.ModelVersions == null) + return; + var installedModels = settingsManager.Settings.InstalledModelHashes; - if (!installedModels.Any()) return; - + if (!installedModels.Any()) + return; + // check if latest version is installed var latestVersion = CivitModel.ModelVersions.FirstOrDefault(); - if (latestVersion == null) return; - - var latestVersionInstalled = latestVersion.Files != null && latestVersion.Files.Any(file => - file is {Type: CivitFileType.Model, Hashes.BLAKE3: not null} && - installedModels.Contains(file.Hashes.BLAKE3)); + if (latestVersion == null) + return; + + var latestVersionInstalled = + latestVersion.Files != null + && latestVersion.Files.Any( + file => + file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } + && installedModels.Contains(file.Hashes.BLAKE3) + ); // check if any of the ModelVersion.Files.Hashes.BLAKE3 hashes are in the installedModels list - var anyVersionInstalled = latestVersionInstalled || CivitModel.ModelVersions.Any(version => - version.Files != null && version.Files.Any(file => - file is {Type: CivitFileType.Model, Hashes.BLAKE3: not null} && - installedModels.Contains(file.Hashes.BLAKE3))); + var anyVersionInstalled = + latestVersionInstalled + || CivitModel.ModelVersions.Any( + version => + version.Files != null + && version.Files.Any( + file => + file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } + && installedModels.Contains(file.Hashes.BLAKE3) + ) + ); - UpdateCardText = latestVersionInstalled ? "Installed" : - anyVersionInstalled ? "Update Available" : string.Empty; + UpdateCardText = latestVersionInstalled + ? "Installed" + : anyVersionInstalled + ? "Update Available" + : string.Empty; ShowUpdateCard = anyVersionInstalled; } @@ -130,7 +152,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel CardImage = new Uri(image.Url); return; } - + // If no valid image found, use no image CardImage = Assets.NoImage; @@ -158,11 +180,16 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel var versions = model.ModelVersions; if (versions is null || versions.Count == 0) { - notificationService.Show(new Notification("Model has no versions available", - "This model has no versions available for download", NotificationType.Warning)); + notificationService.Show( + new Notification( + "Model has no versions available", + "This model has no versions available for download", + NotificationType.Warning + ) + ); return; } - + var dialog = new BetterContentDialog { Title = model.Name, @@ -170,20 +197,27 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel IsSecondaryButtonEnabled = false, IsFooterVisible = false, MaxDialogWidth = 750, + MaxDialogHeight = 850, }; - + + var prunedDescription = PruneDescription(model); + var viewModel = dialogFactory.Get(); viewModel.Dialog = dialog; - viewModel.Versions = versions.Select(version => - new ModelVersionViewModel( - settingsManager.Settings.InstalledModelHashes ?? new HashSet(), version)) + viewModel.Title = model.Name; + viewModel.Description = prunedDescription; + viewModel.Versions = versions + .Select( + version => + new ModelVersionViewModel( + settingsManager.Settings.InstalledModelHashes ?? new HashSet(), + version + ) + ) .ToImmutableArray(); viewModel.SelectedVersionViewModel = viewModel.Versions[0]; - - dialog.Content = new SelectModelVersionDialog - { - DataContext = viewModel - }; + + dialog.Content = new SelectModelVersionDialog { DataContext = viewModel }; var result = await dialog.ShowAsync(); @@ -199,18 +233,34 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel await DoImport(model, selectedVersion, selectedFile); } + private static string PruneDescription(CivitModel model) + { + var prunedDescription = model.Description + .Replace("
", $"{Environment.NewLine}{Environment.NewLine}") + .Replace("
", $"{Environment.NewLine}{Environment.NewLine}") + .Replace("

", $"{Environment.NewLine}{Environment.NewLine}") + .Replace("", $"{Environment.NewLine}{Environment.NewLine}") + .Replace("", $"{Environment.NewLine}{Environment.NewLine}") + .Replace("", $"{Environment.NewLine}{Environment.NewLine}") + .Replace("", $"{Environment.NewLine}{Environment.NewLine}") + .Replace("", $"{Environment.NewLine}{Environment.NewLine}") + .Replace("", $"{Environment.NewLine}{Environment.NewLine}"); + prunedDescription = HtmlRegex().Replace(prunedDescription, string.Empty); + return prunedDescription; + } + private static async Task SaveCmInfo( - CivitModel model, + CivitModel model, CivitModelVersion modelVersion, CivitFile modelFile, - DirectoryPath downloadDirectory) + DirectoryPath downloadDirectory + ) { var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name); - var modelInfo = - new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow); - + var modelInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow); + await modelInfo.SaveJsonToDirectory(downloadDirectory, modelFileName); - + var jsonName = $"{modelFileName}.cm-info.json"; return downloadDirectory.JoinFile(jsonName); } @@ -221,87 +271,103 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel /// /// /// The file path of the saved preview image - private async Task SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath) + private async Task SavePreviewImage( + CivitModelVersion modelVersion, + FilePath modelFilePath + ) { // Skip if model has no images if (modelVersion.Images == null || modelVersion.Images.Count == 0) { return null; } - + var image = modelVersion.Images[0]; var imageExtension = Path.GetExtension(image.Url).TrimStart('.'); if (imageExtension is "jpg" or "jpeg" or "png") { - var imageDownloadPath = - modelFilePath.Directory!.JoinFile($"{modelFilePath.Name}.preview.{imageExtension}"); + var imageDownloadPath = modelFilePath.Directory!.JoinFile( + $"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}" + ); - var imageTask = - downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); + var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); await notificationService.TryAsync(imageTask, "Could not download preview image"); - + return imageDownloadPath; } - + return null; } - - private async Task DoImport(CivitModel model, CivitModelVersion? selectedVersion = null, - CivitFile? selectedFile = null) + + private async Task DoImport( + CivitModel model, + CivitModelVersion? selectedVersion = null, + CivitFile? selectedFile = null + ) { IsImporting = true; Text = "Downloading..."; OnDownloadStart?.Invoke(this); - + // Get latest version var modelVersion = selectedVersion ?? model.ModelVersions?.FirstOrDefault(); if (modelVersion is null) { - notificationService.Show(new Notification("Model has no versions available", - "This model has no versions available for download", NotificationType.Warning)); + notificationService.Show( + new Notification( + "Model has no versions available", + "This model has no versions available for download", + NotificationType.Warning + ) + ); Text = "Unable to Download"; return; } // Get latest version file - var modelFile = selectedFile ?? - modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model); + var modelFile = + selectedFile ?? modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model); if (modelFile is null) { - notificationService.Show(new Notification("Model has no files available", - "This model has no files available for download", NotificationType.Warning)); + notificationService.Show( + new Notification( + "Model has no files available", + "This model has no files available for download", + NotificationType.Warning + ) + ); Text = "Unable to Download"; return; } var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); - - var downloadDirectory = - rootModelsDirectory.JoinDir(model.Type.ConvertTo() - .GetStringValue()); + + var downloadDirectory = rootModelsDirectory.JoinDir( + model.Type.ConvertTo().GetStringValue() + ); // Folders might be missing if user didn't install any packages yet downloadDirectory.Create(); - + var downloadPath = downloadDirectory.JoinFile(modelFile.Name); // Download model info and preview first var cmInfoPath = await SaveCmInfo(model, modelVersion, modelFile, downloadDirectory); var previewImagePath = await SavePreviewImage(modelVersion, downloadPath); - + // Create tracked download var download = trackedDownloadService.NewDownload(modelFile.DownloadUrl, downloadPath); - + // Add hash info download.ExpectedHashSha256 = modelFile.Hashes.SHA256; - + // Add files to cleanup list download.ExtraCleanupFileNames.Add(cmInfoPath); if (previewImagePath is not null) { download.ExtraCleanupFileNames.Add(previewImagePath); } - + // Attach for progress updates download.ProgressUpdate += (s, e) => { @@ -321,7 +387,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel if (e == ProgressState.Success) { Text = "Import Complete"; - + IsIndeterminate = false; Value = 100; CheckIfInstalled(); @@ -338,20 +404,24 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel DelayedClearProgress(TimeSpan.FromMilliseconds(800)); } }; - + // Add hash context action download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(modelFile); - + download.Start(); } private void DelayedClearProgress(TimeSpan delay) { - Task.Delay(delay).ContinueWith(_ => - { - Text = string.Empty; - Value = 0; - IsImporting = false; - }); + Task.Delay(delay) + .ContinueWith(_ => + { + Text = string.Empty; + Value = 0; + IsImporting = false; + }); } + + [GeneratedRegex("<[^>]+>")] + private static partial Regex HtmlRegex(); } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs index 8ecfff45..84e059f3 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -49,11 +48,11 @@ public partial class CheckpointFile : ViewModelBase [ObservableProperty] private bool isLoading; [ObservableProperty] private CivitModelType modelType; - public string FileName => Path.GetFileName((string?) FilePath); + public string FileName => Path.GetFileName(FilePath); public ObservableCollection Badges { get; set; } = new(); - private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", "bin" }; + private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" }; private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" }; private static readonly string[] SupportedMetadataExtensions = { ".json" }; @@ -200,7 +199,8 @@ public partial class CheckpointFile : ViewModelBase { foreach (var file in Directory.EnumerateFiles(directory, "*.*", searchOption)) { - if (!SupportedCheckpointExtensions.Any(ext => file.Contains(ext))) + if (!SupportedCheckpointExtensions.Any(ext => + Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase))) continue; var checkpointFile = new CheckpointFile @@ -230,7 +230,8 @@ public partial class CheckpointFile : ViewModelBase { foreach (var file in Directory.EnumerateFiles(modelsDirectory, "*.*", SearchOption.AllDirectories)) { - if (!SupportedCheckpointExtensions.Any(ext => file.Contains(ext))) + if (!SupportedCheckpointExtensions.Any(ext => + Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase))) continue; var checkpointFile = new CheckpointFile @@ -239,7 +240,7 @@ public partial class CheckpointFile : ViewModelBase FilePath = file, }; - var jsonPath = Path.Combine(Path.GetDirectoryName(file), + var jsonPath = Path.Combine(Path.GetDirectoryName(file) ?? "", Path.GetFileNameWithoutExtension(file) + ".cm-info.json"); if (File.Exists(jsonPath)) @@ -251,7 +252,7 @@ public partial class CheckpointFile : ViewModelBase } checkpointFile.PreviewImagePath = SupportedImageExtensions - .Select(ext => Path.Combine(Path.GetDirectoryName(file), + .Select(ext => Path.Combine(Path.GetDirectoryName(file) ?? "", $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists) .FirstOrDefault(); diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs index 5f33a079..1720bcc3 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs @@ -29,6 +29,8 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase private const string InvalidDirectoryText = "Directory must be empty or have a valid settings.json file"; private const string NotEnoughFreeSpaceText = "Not enough free space on the selected drive"; + private const string FatWarningText = + "FAT32 / exFAT drives are not supported at this time"; [ObservableProperty] private string dataDirectory = DefaultInstallLocation; [ObservableProperty] private bool isPortableMode; @@ -36,6 +38,7 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase [ObservableProperty] private string directoryStatusText = string.Empty; [ObservableProperty] private bool isStatusBadgeVisible; [ObservableProperty] private bool isDirectoryValid; + [ObservableProperty] private bool showFatWarning; public RefreshBadgeViewModel ValidatorRefreshBadge { get; } = new() { @@ -66,6 +69,8 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase private async Task ValidateDataDirectory() { await using var delay = new MinimumDelay(100, 200); + + ShowFatWarning = IsDriveFat(DataDirectory); // Doesn't exist, this is fine as a new install, hide badge if (!Directory.Exists(DataDirectory)) @@ -151,4 +156,18 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase { DataDirectory = value ? Compat.AppCurrentDir + "Data" : DefaultInstallLocation; } + + private bool IsDriveFat(string path) + { + try + { + var drive = new DriveInfo(Path.GetPathRoot(path)); + return drive.DriveFormat.Contains("FAT", StringComparison.OrdinalIgnoreCase); + } + catch (Exception e) + { + Logger.Warn(e, "Error checking drive FATness"); + return false; + } + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs index 082859db..4d643c26 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Media.Imaging; @@ -7,6 +8,7 @@ using Avalonia.Platform; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; @@ -18,11 +20,22 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase public required ContentDialog Dialog { get; set; } public required IReadOnlyList Versions { get; set; } + public required string Description { get; set; } + public required string Title { get; set; } [ObservableProperty] private Bitmap? previewImage; [ObservableProperty] private ModelVersionViewModel? selectedVersionViewModel; [ObservableProperty] private CivitFileViewModel? selectedFile; [ObservableProperty] private bool isImportEnabled; + [ObservableProperty] private ObservableCollection imageUrls = new(); + [ObservableProperty] private bool canGoToNextImage; + [ObservableProperty] private bool canGoToPreviousImage; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayedPageNumber))] + private int selectedImageIndex; + + public int DisplayedPageNumber => SelectedImageIndex + 1; public SelectModelVersionViewModel(ISettingsManager settingsManager, IDownloadService downloadService) @@ -34,18 +47,31 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase public override void OnLoaded() { SelectedVersionViewModel = Versions[0]; + CanGoToNextImage = true; } partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value) { var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled; - var firstImageUrl = value?.ModelVersion?.Images?.FirstOrDefault( - img => nsfwEnabled || img.Nsfw == "None")?.Url; + var allImages = value?.ModelVersion?.Images?.Where( + img => nsfwEnabled || img.Nsfw == "None")?.Select(x => new ImageSource(x.Url)).ToList(); + + if (allImages == null || !allImages.Any()) + { + allImages = new List {new(Assets.NoImage)}; + CanGoToNextImage = false; + } + else + { + CanGoToNextImage = allImages.Count > 1; + } - Dispatcher.UIThread.InvokeAsync(async () => + Dispatcher.UIThread.Post(() => { - SelectedFile = value?.CivitFileViewModels.FirstOrDefault(); - await UpdateImage(firstImageUrl); + CanGoToPreviousImage = false; + SelectedFile = SelectedVersionViewModel?.CivitFileViewModels.FirstOrDefault(); + ImageUrls = new ObservableCollection(allImages); + SelectedImageIndex = 0; }); } @@ -54,15 +80,6 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase IsImportEnabled = value?.CivitFile != null; } - private async Task UpdateImage(string? url = null) - { - var assetStream = string.IsNullOrWhiteSpace(url) - ? AssetLoader.Open(new Uri("avares://StabilityMatrix.Avalonia/Assets/noimage.png")) - : await downloadService.GetImageStreamFromUrl(url); - - PreviewImage = new Bitmap(assetStream); - } - public void Cancel() { Dialog.Hide(ContentDialogResult.Secondary); @@ -72,4 +89,18 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase { Dialog.Hide(ContentDialogResult.Primary); } + + public void PreviousImage() + { + if (SelectedImageIndex > 0) SelectedImageIndex--; + CanGoToPreviousImage = SelectedImageIndex > 0; + CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1; + } + + public void NextImage() + { + if (SelectedImageIndex < ImageUrls.Count - 1) SelectedImageIndex++; + CanGoToPreviousImage = SelectedImageIndex > 0; + CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1; + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs index 99ee8ebd..06dad5be 100644 --- a/StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Notifications; -using CommunityToolkit.Mvvm.ComponentModel; +using Avalonia.Threading; using FluentAvalonia.UI.Controls; -using Polly; -using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views; @@ -53,7 +49,10 @@ public partial class ProgressManagerViewModel : PageViewModelBase switch (state) { case ProgressState.Success: - notificationService.Show("Download Completed", $"Download of {e.FileName} completed successfully.", NotificationType.Success); + Dispatcher.UIThread.Post(() => + { + notificationService.Show("Download Completed", $"Download of {e.FileName} completed successfully.", NotificationType.Success); + }); break; case ProgressState.Failed: var msg = ""; @@ -61,10 +60,16 @@ public partial class ProgressManagerViewModel : PageViewModelBase { msg = $"({exception.GetType().Name}) {exception.Message}"; } - notificationService.ShowPersistent("Download Failed", $"Download of {e.FileName} failed: {msg}", NotificationType.Error); + Dispatcher.UIThread.Post(() => + { + notificationService.ShowPersistent("Download Failed", $"Download of {e.FileName} failed: {msg}", NotificationType.Error); + }); break; case ProgressState.Cancelled: - notificationService.Show("Download Cancelled", $"Download of {e.FileName} was cancelled.", NotificationType.Warning); + Dispatcher.UIThread.Post(() => + { + notificationService.Show("Download Cancelled", $"Download of {e.FileName} was cancelled.", NotificationType.Warning); + }); break; } }; diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml index 2fe90df8..5483e137 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml @@ -55,6 +55,14 @@ Foreground="LightGray" FontSize="12" Margin="0,8,0,0" /> + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -66,6 +118,8 @@ @@ -78,7 +132,7 @@ + Margin="0,4,0,4"> + + + + + + -
public static string RelativePythonDllPath => Compat.Switch( (PlatformKind.Windows, "python310.dll"), - (PlatformKind.Linux, Path.Combine("lib", "libpython3.10.so"))); - - public static string PythonDllPath => Compat.Switch( - (PlatformKind.Windows, Path.Combine(PythonDir, "python310.dll")), - (PlatformKind.Linux, Path.Combine(PythonDir, "lib", "libpython3.10.so"))); + (PlatformKind.Linux, Path.Combine("lib", "libpython3.10.so")), + (PlatformKind.MacOS, Path.Combine("lib", "libpython3.10.dylib"))); + + public static string PythonDllPath => Path.Combine(PythonDir, RelativePythonDllPath); public static string PythonExePath => Compat.Switch( (PlatformKind.Windows, Path.Combine(PythonDir, "python.exe")), - (PlatformKind.Linux, Path.Combine(PythonDir, "bin", "python3"))); + (PlatformKind.Linux, Path.Combine(PythonDir, "bin", "python3")), + (PlatformKind.MacOS, Path.Combine(PythonDir, "bin", "python3"))); public static string PipExePath => Compat.Switch( (PlatformKind.Windows, Path.Combine(PythonDir, "Scripts", "pip.exe")), - (PlatformKind.Linux, Path.Combine(PythonDir, "bin", "pip3"))); + (PlatformKind.Linux, Path.Combine(PythonDir, "bin", "pip3")), + (PlatformKind.MacOS, Path.Combine(PythonDir, "bin", "pip3"))); public static string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc"); diff --git a/StabilityMatrix.Core/Python/PyVenvRunner.cs b/StabilityMatrix.Core/Python/PyVenvRunner.cs index e50de47c..84aa0dfb 100644 --- a/StabilityMatrix.Core/Python/PyVenvRunner.cs +++ b/StabilityMatrix.Core/Python/PyVenvRunner.cs @@ -24,6 +24,13 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable public const string TorchPipInstallArgsCpu = "torch torchvision torchaudio"; public const string TorchPipInstallArgsDirectML = "torch-directml"; + public const string TorchPipInstallArgsRocm511 = + "torch torchvision --extra-index-url https://download.pytorch.org/whl/rocm5.1.1"; + public const string TorchPipInstallArgsRocm542 = + "torch torchvision --extra-index-url https://download.pytorch.org/whl/rocm5.4.2"; + public const string TorchPipInstallArgsRocmNightly56 = + "--pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm5.6"; + /// /// Relative path to the site-packages folder from the venv root. /// This is platform specific. diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index 69700e49..308d383a 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -247,7 +247,9 @@ public class SettingsManager : ISettingsManager { var libraryJson = libraryJsonFile.ReadAllText(); var librarySettings = JsonSerializer.Deserialize(libraryJson); - if (!string.IsNullOrWhiteSpace(librarySettings?.LibraryPath)) + + if (!string.IsNullOrWhiteSpace(librarySettings?.LibraryPath) + && Directory.Exists(librarySettings?.LibraryPath)) { LibraryDir = librarySettings.LibraryPath; SetStaticLibraryPaths();