From b9cb81e9a9fc9f5335f2af60e5e9eeaa759d07a0 Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 27 Aug 2023 00:23:10 -0700 Subject: [PATCH 01/29] install/update/versioning refactor & new install dialog layout wip --- .../DesignData/DesignData.cs | 12 +- .../ViewModels/Dialogs/InstallerViewModel.cs | 83 ++-- .../Dialogs/OneClickInstallViewModel.cs | 39 +- .../Dialogs/PackageImportViewModel.cs | 49 +- .../PackageManager/PackageCardViewModel.cs | 6 +- .../Views/Dialogs/InstallerDialog.axaml | 425 ++++++++---------- .../Models/DownloadPackageVersionOptions.cs | 8 + .../Models/InstalledPackage.cs | 44 +- .../Models/InstalledPackageVersion.cs | 20 + .../Models/Packages/A3WebUI.cs | 10 +- .../Models/Packages/BaseGitPackage.cs | 128 +++--- .../Models/Packages/BasePackage.cs | 25 +- .../Models/Packages/ComfyUI.cs | 18 +- .../Models/Packages/DankDiffusion.cs | 5 - .../Models/Packages/Fooocus.cs | 6 +- .../Models/Packages/InvokeAI.cs | 46 +- .../Models/Packages/PackageVersionOptions.cs | 7 + .../Models/Packages/UnknownPackage.cs | 11 +- .../Models/Packages/VladAutomatic.cs | 65 +-- .../Models/Packages/VoltaML.cs | 8 +- .../Models/SharedFolderMethod.cs | 8 + .../Services/ISettingsManager.cs | 2 +- .../Services/SettingsManager.cs | 8 +- .../Services/TrackedDownloadService.cs | 4 +- 24 files changed, 556 insertions(+), 481 deletions(-) create mode 100644 StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs create mode 100644 StabilityMatrix.Core/Models/InstalledPackageVersion.cs create mode 100644 StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs create mode 100644 StabilityMatrix.Core/Models/SharedFolderMethod.cs diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index b3b3379e..7d378412 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -54,9 +54,11 @@ public static class DesignData { Id = activePackageId, DisplayName = "My Installed Package", - DisplayVersion = "v1.0.0", PackageName = "stable-diffusion-webui", - PackageVersion = "v1.0.0", + Version = new InstalledPackageVersion + { + InstalledReleaseVersion = "v1.0.0" + }, LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LastUpdateCheck = DateTimeOffset.Now }, @@ -65,7 +67,11 @@ public static class DesignData Id = Guid.NewGuid(), DisplayName = "Dank Diffusion", PackageName = "ComfyUI", - DisplayVersion = "main@ab73d4a", + Version = new InstalledPackageVersion + { + InstalledBranch = "master", + InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8" + }, LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LastUpdateCheck = DateTimeOffset.Now } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs index d9f99a7b..f44ca8bf 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs @@ -42,14 +42,13 @@ public partial class InstallerViewModel : ContentDialogViewModelBase [ObservableProperty] private BasePackage selectedPackage; [ObservableProperty] private PackageVersion? selectedVersion; - [ObservableProperty] private IReadOnlyList? availablePackages; [ObservableProperty] private ObservableCollection? availableCommits; [ObservableProperty] private ObservableCollection? availableVersions; - [ObservableProperty] private GitCommit? selectedCommit; - [ObservableProperty] private string? releaseNotes; + [ObservableProperty] private string latestVersionText = string.Empty; + [ObservableProperty] private bool isAdvancedMode; // Version types (release or commit) [ObservableProperty] @@ -107,23 +106,19 @@ public partial class InstallerViewModel : ContentDialogViewModelBase // Check for updates try { + var versionOptions = await SelectedPackage.GetAllVersionOptions(); if (IsReleaseMode) { - var versions = (await SelectedPackage.GetAllVersions()).ToList(); - AvailableVersions = new ObservableCollection(versions); + AvailableVersions = + new ObservableCollection(versionOptions.AvailableVersions); if (!AvailableVersions.Any()) return; SelectedVersion = AvailableVersions[0]; } else { - var branches = (await SelectedPackage.GetAllBranches()).ToList(); - AvailableVersions = new ObservableCollection(branches.Select(b => - new PackageVersion - { - TagName = b.Name, - ReleaseNotesMarkdown = b.Commit.Label - })); + AvailableVersions = + new ObservableCollection(versionOptions.AvailableBranches); UpdateSelectedVersionToLatestMain(); } @@ -173,9 +168,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase try { await InstallGitIfNecessary(); - - SelectedPackage.InstallLocation = Path.Combine( - settingsManager.LibraryDir, "Packages", InstallName); + var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", InstallName); if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled) { @@ -193,45 +186,41 @@ public partial class InstallerViewModel : ContentDialogViewModelBase } } - string version; + var downloadOptions = new DownloadPackageVersionOptions(); + var installedVersion = new InstalledPackageVersion(); if (IsReleaseMode) { - version = SelectedVersion?.TagName ?? - throw new NullReferenceException("Selected version is null"); - - await DownloadPackage(version, false, null); + downloadOptions.VersionTag = SelectedVersion?.TagName ?? + throw new NullReferenceException("Selected version is null"); + installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag; } else { - version = SelectedCommit?.Sha ?? + downloadOptions.CommitHash = SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null"); - - await DownloadPackage(version, true, SelectedVersion!.TagName); + installedVersion.InstalledBranch = SelectedVersion?.TagName ?? + throw new NullReferenceException("Selected version is null"); + installedVersion.InstalledCommitSha = downloadOptions.CommitHash; } - await InstallPackage(); + await DownloadPackage(installLocation, downloadOptions); + await InstallPackage(installLocation); InstallProgress.Text = "Setting up shared folder links..."; - await SelectedPackage.SetupModelFolders(SelectedPackage.InstallLocation); - //sharedFolders.SetupLinksForPackage(SelectedPackage, SelectedPackage.InstallLocation); + await SelectedPackage.SetupModelFolders(installLocation); InstallProgress.Text = "Done"; InstallProgress.IsIndeterminate = false; InstallProgress.Value = 100; EventManager.Instance.OnGlobalProgressChanged(100); - var branch = SelectedVersionType == PackageVersionType.GithubRelease ? - null : SelectedVersion!.TagName; - var package = new InstalledPackage { DisplayName = InstallName, LibraryPath = Path.Combine("Packages", InstallName), Id = Guid.NewGuid(), PackageName = SelectedPackage.Name, - PackageVersion = version, - DisplayVersion = GetDisplayVersion(version, branch), - InstalledBranch = branch, + Version = installedVersion, LaunchCommand = SelectedPackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now }; @@ -270,22 +259,22 @@ public partial class InstallerViewModel : ContentDialogViewModelBase { return branch == null ? version : $"{branch}@{version[..7]}"; } - - private Task DownloadPackage(string version, bool isCommitHash, string? branch) + + private async Task DownloadPackage(string installLocation, DownloadPackageVersionOptions downloadOptions) { InstallProgress.Text = "Downloading package..."; - + var progress = new Progress(progress => { InstallProgress.IsIndeterminate = progress.IsIndeterminate; InstallProgress.Value = progress.Percentage; EventManager.Instance.OnGlobalProgressChanged((int) progress.Percentage); }); - - return SelectedPackage.DownloadPackage(version, isCommitHash, branch, progress); + + await SelectedPackage.DownloadPackage(installLocation, downloadOptions, progress); } - private async Task InstallPackage() + private async Task InstallPackage(string installLocation) { InstallProgress.Text = "Installing package..."; SelectedPackage.ConsoleOutput += SelectedPackageOnConsoleOutput; @@ -298,7 +287,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase EventManager.Instance.OnGlobalProgressChanged((int) progress.Percentage); }); - await SelectedPackage.InstallPackage(progress); + await SelectedPackage.InstallPackage(installLocation, progress); } finally { @@ -360,20 +349,23 @@ public partial class InstallerViewModel : ContentDialogViewModelBase AvailableVersionTypes = SelectedPackage.ShouldIgnoreReleases ? PackageVersionType.Commit : PackageVersionType.GithubRelease | PackageVersionType.Commit; + + IsReleaseMode = !SelectedPackage.ShouldIgnoreReleases; if (Design.IsDesignMode) return; Dispatcher.UIThread.InvokeAsync(async () => { Logger.Debug($"Release mode: {IsReleaseMode}"); - var versions = (await value.GetAllVersions(IsReleaseMode)).ToList(); + var versionOptions = await value.GetAllVersionOptions(); - if (!versions.Any()) return; + AvailableVersions = IsReleaseModeAvailable + ? new ObservableCollection(versionOptions.AvailableVersions) + : new ObservableCollection(versionOptions.AvailableBranches); - AvailableVersions = new ObservableCollection(versions); - Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}"); SelectedVersion = AvailableVersions[0]; - ReleaseNotes = versions.First().ReleaseNotesMarkdown; + ReleaseNotes = SelectedVersion.ReleaseNotesMarkdown; + Logger.Debug($"Loaded release notes for {ReleaseNotes}"); if (!IsReleaseMode) @@ -387,6 +379,9 @@ public partial class InstallerViewModel : ContentDialogViewModelBase } InstallName = SelectedPackage.DisplayName; + LatestVersionText = IsReleaseMode + ? $"Latest version: {SelectedVersion.TagName}" + : $"Branch: {SelectedVersion.TagName}"; }).SafeFireAndForget(); } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs index e2ff39cb..8c0df723 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs @@ -107,16 +107,30 @@ public partial class OneClickInstallViewModel : ViewModelBase // get latest version & download & install SubHeaderText = "Getting latest version..."; - var latestVersion = await SelectedPackage.GetLatestVersion(); - SelectedPackage.InstallLocation = - Path.Combine(libraryDir, "Packages", SelectedPackage.Name); + var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name); SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text; + + var downloadVersion = new DownloadPackageVersionOptions(); + var installedVersion = new InstalledPackageVersion(); + + var versionOptions = await SelectedPackage.GetAllVersionOptions(); + if (versionOptions.AvailableVersions != null && versionOptions.AvailableVersions.Any()) + { + downloadVersion.VersionTag = + versionOptions.AvailableVersions.First().TagName; + installedVersion.InstalledReleaseVersion = downloadVersion.VersionTag; + } + else + { + downloadVersion.BranchName = await SelectedPackage.GetLatestVersion(); + installedVersion.InstalledBranch = downloadVersion.BranchName; + } - await DownloadPackage(latestVersion); - await InstallPackage(); + await DownloadPackage(installLocation, downloadVersion); + await InstallPackage(installLocation); SubHeaderText = "Setting up shared folder links..."; - await SelectedPackage.SetupModelFolders(SelectedPackage.InstallLocation); + await SelectedPackage.SetupModelFolders(installLocation); var installedPackage = new InstalledPackage { @@ -124,8 +138,7 @@ public partial class OneClickInstallViewModel : ViewModelBase LibraryPath = Path.Combine("Packages", SelectedPackage.Name), Id = Guid.NewGuid(), PackageName = SelectedPackage.Name, - PackageVersion = latestVersion, - DisplayVersion = latestVersion, + Version = installedVersion, LaunchCommand = SelectedPackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now }; @@ -148,7 +161,7 @@ public partial class OneClickInstallViewModel : ViewModelBase EventManager.Instance.OnOneClickInstallFinished(false); } - private async Task DownloadPackage(string version) + private async Task DownloadPackage(string installLocation, DownloadPackageVersionOptions versionOptions) { SubHeaderText = "Downloading package..."; @@ -158,13 +171,13 @@ public partial class OneClickInstallViewModel : ViewModelBase OneClickInstallProgress = Convert.ToInt32(progress.Percentage); EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress); }); - - await SelectedPackage.DownloadPackage(version, false, version, progress); + + await SelectedPackage.DownloadPackage(installLocation, versionOptions, progress); SubHeaderText = "Download Complete"; OneClickInstallProgress = 100; } - private async Task InstallPackage() + private async Task InstallPackage(string installLocation) { SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text; SubHeaderText = "Downloading and installing package requirements..."; @@ -177,6 +190,6 @@ public partial class OneClickInstallViewModel : ViewModelBase EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress); }); - await SelectedPackage.InstallPackage(progress); + await SelectedPackage.InstallPackage(installLocation, progress); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs index 428f5635..7edbceaf 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs @@ -77,23 +77,19 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase // Populate available versions try { + var versionOptions = await SelectedBasePackage.GetAllVersionOptions(); if (IsReleaseMode) { - var versions = (await SelectedBasePackage.GetAllVersions()).ToList(); - AvailableVersions = new ObservableCollection(versions); + AvailableVersions = + new ObservableCollection(versionOptions.AvailableVersions); if (!AvailableVersions.Any()) return; SelectedVersion = AvailableVersions[0]; } else { - var branches = (await SelectedBasePackage.GetAllBranches()).ToList(); - AvailableVersions = new ObservableCollection(branches.Select(b => - new PackageVersion - { - TagName = b.Name, - ReleaseNotesMarkdown = b.Commit.Label - })); + AvailableVersions = + new ObservableCollection(versionOptions.AvailableBranches); UpdateSelectedVersionToLatestMain(); } } @@ -134,20 +130,19 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase AvailableVersions?.Clear(); AvailableCommits?.Clear(); - AvailableVersionTypes = SelectedBasePackage.ShouldIgnoreReleases - ? PackageVersionType.Commit - : PackageVersionType.GithubRelease | PackageVersionType.Commit; + AvailableVersionTypes = SelectedBasePackage.AvailableVersionTypes; if (Design.IsDesignMode) return; Dispatcher.UIThread.InvokeAsync(async () => { Logger.Debug($"Release mode: {IsReleaseMode}"); - var versions = (await value.GetAllVersions(IsReleaseMode)).ToList(); - - if (!versions.Any()) return; + var versionOptions = await value.GetAllVersionOptions(); - AvailableVersions = new ObservableCollection(versions); + AvailableVersions = IsReleaseModeAvailable + ? new ObservableCollection(versionOptions.AvailableVersions) + : new ObservableCollection(versionOptions.AvailableBranches); + Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}"); SelectedVersion = AvailableVersions[0]; @@ -188,30 +183,30 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase if (SelectedBasePackage is null || PackagePath is null) return; - string version; + var version = new InstalledPackageVersion(); if (IsReleaseMode) { - version = SelectedVersion?.TagName ?? - throw new NullReferenceException("Selected version is null"); + version.InstalledReleaseVersion = SelectedVersion?.TagName ?? + throw new NullReferenceException( + "Selected version is null"); } else { - version = SelectedCommit?.Sha ?? - throw new NullReferenceException("Selected commit is null"); + version.InstalledBranch = SelectedVersion?.TagName ?? + throw new NullReferenceException( + "Selected version is null"); + version.InstalledCommitSha = SelectedCommit?.Sha ?? + throw new NullReferenceException( + "Selected commit is null"); } - var branch = SelectedVersionType == PackageVersionType.GithubRelease ? - null : SelectedVersion!.TagName; - var package = new InstalledPackage { Id = Guid.NewGuid(), DisplayName = PackagePath.Name, PackageName = SelectedBasePackage.Name, LibraryPath = $"Packages{Path.DirectorySeparatorChar}{PackagePath.Name}", - PackageVersion = version, - DisplayVersion = GetDisplayVersion(version, branch), - InstalledBranch = branch, + Version = version, LaunchCommand = SelectedBasePackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now, }; diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs index 04f33294..2398a06d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs @@ -74,7 +74,7 @@ public partial class PackageCardViewModel : ProgressViewModel var basePackage = packageFactory[value.PackageName]; CardImageSource = basePackage?.PreviewImageUri.ToString() ?? Assets.NoImage.ToString(); - InstalledVersion = value.DisplayVersion ?? "Unknown"; + InstalledVersion = value.Version?.DisplayVersion ?? "Unknown"; } } @@ -168,8 +168,6 @@ public partial class PackageCardViewModel : ProgressViewModel try { - basePackage.InstallLocation = Package.FullPath!; - var progress = new Progress(progress => { var percent = Convert.ToInt32(progress.Percentage); @@ -195,7 +193,7 @@ public partial class PackageCardViewModel : ProgressViewModel Package.UpdateAvailable = false; } IsUpdateAvailable = false; - InstalledVersion = Package.DisplayVersion ?? "Unknown"; + InstalledVersion = updateResult.DisplayVersion ?? "Unknown"; EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, packageName, diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml index 36961405..5a4b63f9 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml @@ -1,249 +1,204 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" + xmlns:ui="using:FluentAvalonia.UI.Controls" + xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" + xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs" + xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core" + xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" + xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" + x:DataType="dialogs:InstallerViewModel" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700" + d:DataContext="{x:Static mocks:DesignData.InstallerViewModel}" + x:Class="StabilityMatrix.Avalonia.Views.Dialogs.InstallerDialog"> - - - - - - - - - - - - - - - - - - - - + Foreground="{DynamicResource ThemeRedColor}" + Margin="0,8,8,8" + TextAlignment="Left" + TextWrapping="Wrap"> + + + + + - - - - + + + + + + + + + + + - - - + + + + + string.IsNullOrWhiteSpace(Sha) ? string.Empty : Sha[..7]; } From bb686a581f4981562643403051fb1aa634eba74d Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 31 Aug 2023 11:27:30 -0700 Subject: [PATCH 03/29] Added SharedFolderMethod & TorchVersion options to Advanced Install Options --- StabilityMatrix.Avalonia/App.axaml.cs | 6 +- .../Controls/BetterContentDialog.cs | 6 + .../ContentDialogProgressViewModelBase.cs | 26 ++ .../ContentDialogViewModelBase.cs | 3 +- .../ViewModels/Dialogs/EnvVarsViewModel.cs | 1 + .../ViewModels/Dialogs/InstallerViewModel.cs | 222 ++++++----------- .../Dialogs/LaunchOptionsViewModel.cs | 1 + .../Dialogs/OneClickInstallViewModel.cs | 17 +- .../Dialogs/PackageImportViewModel.cs | 8 +- .../PackageModificationDialogViewModel.cs | 60 +++++ .../Dialogs/SelectDataDirectoryViewModel.cs | 1 + .../Dialogs/SelectModelVersionViewModel.cs | 1 + .../ViewModels/Dialogs/UpdateViewModel.cs | 1 + .../ViewModels/LaunchPageViewModel.cs | 3 +- .../PackageManager/PackageCardViewModel.cs | 7 +- .../ViewModels/PackageManagerViewModel.cs | 33 ++- .../Views/Dialogs/InstallerDialog.axaml | 234 +++++++++--------- .../Dialogs/PackageModificationDialog.axaml | 41 +++ .../PackageModificationDialog.axaml.cs | 49 ++++ StabilityMatrix.Core/Helper/SharedFolders.cs | 5 +- .../Models/InstalledPackage.cs | 15 +- .../AddInstalledPackageStep.cs | 26 ++ .../DownloadPackageVersionStep.cs | 24 ++ .../IPackageModificationRunner.cs | 12 + .../PackageModification/InstallPackageStep.cs | 33 +++ .../PackageModificationRunner.cs | 31 +++ .../Models/PackageModification/PackageStep.cs | 9 + .../SetupModelFoldersStep.cs | 28 +++ .../SetupPrerequisitesStep.cs | 43 ++++ StabilityMatrix.Core/Models/PackageVersion.cs | 1 + .../Models/Packages/A3WebUI.cs | 97 +++++--- .../Models/Packages/BaseGitPackage.cs | 45 ++-- .../Models/Packages/BasePackage.cs | 93 ++++++- .../Models/Packages/ComfyUI.cs | 124 +++++++--- .../Models/Packages/DankDiffusion.cs | 13 +- .../Models/Packages/Fooocus.cs | 68 ++++- .../Models/Packages/InvokeAI.cs | 120 +++++---- .../Models/Packages/UnknownPackage.cs | 26 +- .../Models/Packages/VladAutomatic.cs | 151 ++++++++--- .../Models/Packages/VoltaML.cs | 34 ++- .../Models/SharedFolderMethod.cs | 2 +- StabilityMatrix.Core/Models/TorchVersion.cs | 11 + .../StabilityMatrix.Core.csproj | 1 + 43 files changed, 1230 insertions(+), 502 deletions(-) create mode 100644 StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs rename StabilityMatrix.Avalonia/ViewModels/{Dialogs => Base}/ContentDialogViewModelBase.cs (88%) create mode 100644 StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml create mode 100644 StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs create mode 100644 StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs create mode 100644 StabilityMatrix.Core/Models/PackageModification/DownloadPackageVersionStep.cs create mode 100644 StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs create mode 100644 StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs create mode 100644 StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs create mode 100644 StabilityMatrix.Core/Models/PackageModification/PackageStep.cs create mode 100644 StabilityMatrix.Core/Models/PackageModification/SetupModelFoldersStep.cs create mode 100644 StabilityMatrix.Core/Models/PackageModification/SetupPrerequisitesStep.cs create mode 100644 StabilityMatrix.Core/Models/TorchVersion.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 78baebba..2a5f4e8e 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -53,6 +53,7 @@ using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Configs; +using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Python; @@ -322,6 +323,7 @@ public sealed class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + //services.AddSingleton(); } private static IServiceCollection ConfigureServices() @@ -346,6 +348,7 @@ public sealed class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(provider => @@ -391,7 +394,8 @@ public sealed class App : Application // if (string.IsNullOrWhiteSpace(githubApiKey)) // return client; // - // client.Credentials = new Credentials(githubApiKey); + // client.Credentials = + // new Credentials(""); return client; }); diff --git a/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs b/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs index 732715a3..43d1a5d6 100644 --- a/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs +++ b/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs @@ -156,6 +156,12 @@ public class BetterContentDialog : ContentDialog viewModel.SecondaryButtonClick += OnDialogButtonClick; viewModel.CloseButtonClick += OnDialogButtonClick; } + else if ((Content as Control)?.DataContext is ContentDialogProgressViewModelBase progressViewModel) + { + progressViewModel.PrimaryButtonClick += OnDialogButtonClick; + progressViewModel.SecondaryButtonClick += OnDialogButtonClick; + progressViewModel.CloseButtonClick += OnDialogButtonClick; + } // If commands provided, bind OnCanExecuteChanged to hide buttons // otherwise link visibility to IsEnabled diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs new file mode 100644 index 00000000..eb775e18 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs @@ -0,0 +1,26 @@ +using System; +using FluentAvalonia.UI.Controls; + +namespace StabilityMatrix.Avalonia.ViewModels.Base; + +public class ContentDialogProgressViewModelBase : ProgressViewModel +{ + public event EventHandler? PrimaryButtonClick; + public event EventHandler? SecondaryButtonClick; + public event EventHandler? CloseButtonClick; + + public virtual void OnPrimaryButtonClick() + { + PrimaryButtonClick?.Invoke(this, ContentDialogResult.Primary); + } + + public virtual void OnSecondaryButtonClick() + { + SecondaryButtonClick?.Invoke(this, ContentDialogResult.Secondary); + } + + public virtual void OnCloseButtonClick() + { + CloseButtonClick?.Invoke(this, ContentDialogResult.None); + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ContentDialogViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs similarity index 88% rename from StabilityMatrix.Avalonia/ViewModels/Dialogs/ContentDialogViewModelBase.cs rename to StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs index 61a139f1..c82b6721 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ContentDialogViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs @@ -1,8 +1,7 @@ using System; using FluentAvalonia.UI.Controls; -using StabilityMatrix.Avalonia.ViewModels.Base; -namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; +namespace StabilityMatrix.Avalonia.ViewModels.Base; public class ContentDialogViewModelBase : ViewModelBase { diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs index 52b255f5..5c96fa80 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Avalonia.Collections; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs index da6f0671..808bb078 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs @@ -17,10 +17,12 @@ using FluentAvalonia.UI.Controls; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; @@ -49,6 +51,13 @@ public partial class InstallerViewModel : ContentDialogViewModelBase [ObservableProperty] private string? releaseNotes; [ObservableProperty] private string latestVersionText = string.Empty; [ObservableProperty] private bool isAdvancedMode; + [ObservableProperty] private bool showDuplicateWarning; + [ObservableProperty] private string? installName; + [ObservableProperty] private SharedFolderMethod selectedSharedFolderMethod; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowTorchVersionOptions))] + private TorchVersion selectedTorchVersion; // Version types (release or commit) [ObservableProperty] @@ -60,20 +69,19 @@ public partial class InstallerViewModel : ContentDialogViewModelBase [NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))] private PackageVersionType availableVersionTypes = PackageVersionType.GithubRelease | PackageVersionType.Commit; + + public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch"; public bool IsReleaseMode { get => SelectedVersionType == PackageVersionType.GithubRelease; set => SelectedVersionType = value ? PackageVersionType.GithubRelease : PackageVersionType.Commit; } - public bool IsReleaseModeAvailable => AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease); + public bool ShowTorchVersionOptions => SelectedTorchVersion != TorchVersion.None; - [ObservableProperty] private bool showDuplicateWarning; - - [ObservableProperty] private string? installName; - - public Base.ProgressViewModel InstallProgress { get; } = new(); + public ProgressViewModel InstallProgress { get; } = new(); + public IEnumerable Steps { get; set; } public InstallerViewModel( ISettingsManager settingsManager, @@ -113,7 +121,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase new ObservableCollection(versionOptions.AvailableVersions); if (!AvailableVersions.Any()) return; - SelectedVersion = AvailableVersions[0]; + SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease); } else { @@ -136,9 +144,6 @@ public partial class InstallerViewModel : ContentDialogViewModelBase var result = await notificationService.TryAsync(ActuallyInstall(), "Could not install package"); if (result.IsSuccessful) { - notificationService.Show(new Notification( - $"Package {SelectedPackage.Name} installed successfully!", - "Success", NotificationType.Success)); OnPrimaryButtonClick(); } else @@ -156,83 +161,72 @@ public partial class InstallerViewModel : ContentDialogViewModelBase } } - private async Task ActuallyInstall() + private Task ActuallyInstall() { if (string.IsNullOrWhiteSpace(InstallName)) { notificationService.Show(new Notification("Package name is empty", "Please enter a name for the package", NotificationType.Error)); - return; + return Task.CompletedTask; } - try + var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", InstallName); + var prereqStep = new SetupPrerequisitesStep(prerequisiteHelper, pyRunner); + + var downloadOptions = new DownloadPackageVersionOptions(); + var installedVersion = new InstalledPackageVersion(); + if (IsReleaseMode) { - await InstallGitIfNecessary(); - var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", InstallName); - - if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled) - { - InstallProgress.Text = "Installing dependencies..."; - InstallProgress.IsIndeterminate = true; - await pyRunner.Initialize(); - - if (!PyRunner.PipInstalled) - { - await pyRunner.SetupPip(); - } - if (!PyRunner.VenvInstalled) - { - await pyRunner.InstallPackage("virtualenv"); - } - } + downloadOptions.VersionTag = SelectedVersion?.TagName ?? + throw new NullReferenceException("Selected version is null"); + installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag; + } + else + { + downloadOptions.CommitHash = SelectedCommit?.Sha ?? + throw new NullReferenceException("Selected commit is null"); + installedVersion.InstalledBranch = SelectedVersion?.TagName ?? + throw new NullReferenceException("Selected version is null"); + installedVersion.InstalledCommitSha = downloadOptions.CommitHash; + } - var downloadOptions = new DownloadPackageVersionOptions(); - var installedVersion = new InstalledPackageVersion(); - if (IsReleaseMode) - { - downloadOptions.VersionTag = SelectedVersion?.TagName ?? - throw new NullReferenceException("Selected version is null"); - installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag; - } - else - { - downloadOptions.CommitHash = SelectedCommit?.Sha ?? - throw new NullReferenceException("Selected commit is null"); - installedVersion.InstalledBranch = SelectedVersion?.TagName ?? - throw new NullReferenceException("Selected version is null"); - installedVersion.InstalledCommitSha = downloadOptions.CommitHash; - } - - await DownloadPackage(installLocation, downloadOptions); - await InstallPackage(installLocation); + var downloadStep = + new DownloadPackageVersionStep(SelectedPackage, installLocation, downloadOptions); + var installStep = new InstallPackageStep(SelectedPackage, SelectedTorchVersion, installLocation); + var setupModelFoldersStep = new SetupModelFoldersStep(SelectedPackage, + SelectedSharedFolderMethod, installLocation); - InstallProgress.Text = "Setting up shared folder links..."; - await SelectedPackage.SetupModelFolders(installLocation); - - InstallProgress.Text = "Done"; - InstallProgress.IsIndeterminate = false; - InstallProgress.Value = 100; - EventManager.Instance.OnGlobalProgressChanged(100); + var package = new InstalledPackage + { + DisplayName = InstallName, + LibraryPath = Path.Combine("Packages", InstallName), + Id = Guid.NewGuid(), + PackageName = SelectedPackage.Name, + Version = installedVersion, + LaunchCommand = SelectedPackage.LaunchCommand, + LastUpdateCheck = DateTimeOffset.Now, + PreferredTorchVersion = SelectedTorchVersion, + PreferredSharedFolderMethod = SelectedSharedFolderMethod + }; - var package = new InstalledPackage - { - DisplayName = InstallName, - LibraryPath = Path.Combine("Packages", InstallName), - Id = Guid.NewGuid(), - PackageName = SelectedPackage.Name, - Version = installedVersion, - LaunchCommand = SelectedPackage.LaunchCommand, - LastUpdateCheck = DateTimeOffset.Now - }; - await using var st = settingsManager.BeginTransaction(); - st.Settings.InstalledPackages.Add(package); - st.Settings.ActiveInstalledPackageId = package.Id; - } - finally + var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, package); + + var steps = new List { - InstallProgress.Value = 0; - InstallProgress.IsIndeterminate = false; - } + prereqStep, + downloadStep, + installStep, + setupModelFoldersStep, + addInstalledPackageStep + }; + + Steps = steps; + return Task.CompletedTask; + } + + public void Cancel() + { + OnCloseButtonClick(); } private void UpdateSelectedVersionToLatestMain() @@ -255,51 +249,6 @@ public partial class InstallerViewModel : ContentDialogViewModelBase } } - private static string GetDisplayVersion(string version, string? branch) - { - return branch == null ? version : $"{branch}@{version[..7]}"; - } - - private async Task DownloadPackage(string installLocation, DownloadPackageVersionOptions downloadOptions) - { - InstallProgress.Text = "Downloading package..."; - - var progress = new Progress(progress => - { - InstallProgress.IsIndeterminate = progress.IsIndeterminate; - InstallProgress.Value = progress.Percentage; - EventManager.Instance.OnGlobalProgressChanged((int) progress.Percentage); - }); - - await SelectedPackage.DownloadPackage(installLocation, downloadOptions, progress); - } - - private async Task InstallPackage(string installLocation) - { - InstallProgress.Text = "Installing package..."; - SelectedPackage.ConsoleOutput += SelectedPackageOnConsoleOutput; - try - { - var progress = new Progress(progress => - { - InstallProgress.IsIndeterminate = progress.IsIndeterminate; - InstallProgress.Value = progress.Percentage; - EventManager.Instance.OnGlobalProgressChanged((int) progress.Percentage); - }); - - await SelectedPackage.InstallPackage(installLocation, progress); - } - finally - { - SelectedPackage.ConsoleOutput -= SelectedPackageOnConsoleOutput; - } - } - - private void SelectedPackageOnConsoleOutput(object? sender, ProcessOutput e) - { - InstallProgress.Description = e.Text; - } - [RelayCommand] private async Task ShowPreview() { @@ -350,6 +299,9 @@ public partial class InstallerViewModel : ContentDialogViewModelBase ? PackageVersionType.Commit : PackageVersionType.GithubRelease | PackageVersionType.Commit; + SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod; + SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion(); + if (Design.IsDesignMode) return; Dispatcher.UIThread.InvokeAsync(async () => @@ -361,7 +313,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase ? new ObservableCollection(versionOptions.AvailableVersions) : new ObservableCollection(versionOptions.AvailableBranches); - SelectedVersion = AvailableVersions[0]; + SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease); ReleaseNotes = SelectedVersion.ReleaseNotesMarkdown; Logger.Debug($"Loaded release notes for {ReleaseNotes}"); @@ -383,34 +335,6 @@ public partial class InstallerViewModel : ContentDialogViewModelBase }).SafeFireAndForget(); } - private async Task InstallGitIfNecessary() - { - var progressHandler = new Progress(progress => - { - if (progress.Message != null && progress.Message.Contains("Downloading")) - { - InstallProgress.Text = $"Downloading prerequisites... {progress.Percentage:N0}%"; - } - else if (progress.Type == ProgressType.Extract) - { - InstallProgress.Text = $"Installing git... {progress.Percentage:N0}%"; - } - else if (progress.Title != null && progress.Title.Contains("Unpacking")) - { - InstallProgress.Text = $"Unpacking resources... {progress.Percentage:N0}%"; - } - else - { - InstallProgress.Text = progress.Message; - } - - InstallProgress.IsIndeterminate = progress.IsIndeterminate; - InstallProgress.Value = Convert.ToInt32(progress.Percentage); - }); - - await prerequisiteHelper.InstallAllIfNecessary(progressHandler); - } - partial void OnInstallNameChanged(string? value) { ShowDuplicateWarning = diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs index 9979fd6f..a4d2e4a6 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs @@ -8,6 +8,7 @@ using System.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FuzzySharp; using Microsoft.Extensions.Logging; +using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper.Cache; diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs index 8c0df723..40aff597 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs @@ -126,11 +126,14 @@ public partial class OneClickInstallViewModel : ViewModelBase installedVersion.InstalledBranch = downloadVersion.BranchName; } - await DownloadPackage(installLocation, downloadVersion); - await InstallPackage(installLocation); + var torchVersion = SelectedPackage.GetRecommendedTorchVersion(); + await DownloadPackage(installLocation, downloadVersion); + await InstallPackage(installLocation, torchVersion); + SubHeaderText = "Setting up shared folder links..."; - await SelectedPackage.SetupModelFolders(installLocation); + var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod; + await SelectedPackage.SetupModelFolders(installLocation, recommendedSharedFolderMethod); var installedPackage = new InstalledPackage { @@ -140,7 +143,9 @@ public partial class OneClickInstallViewModel : ViewModelBase PackageName = SelectedPackage.Name, Version = installedVersion, LaunchCommand = SelectedPackage.LaunchCommand, - LastUpdateCheck = DateTimeOffset.Now + LastUpdateCheck = DateTimeOffset.Now, + PreferredTorchVersion = torchVersion, + PreferredSharedFolderMethod = recommendedSharedFolderMethod }; await using var st = settingsManager.BeginTransaction(); st.Settings.InstalledPackages.Add(installedPackage); @@ -177,7 +182,7 @@ public partial class OneClickInstallViewModel : ViewModelBase OneClickInstallProgress = 100; } - private async Task InstallPackage(string installLocation) + private async Task InstallPackage(string installLocation, TorchVersion torchVersion) { SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text; SubHeaderText = "Downloading and installing package requirements..."; @@ -190,6 +195,6 @@ public partial class OneClickInstallViewModel : ViewModelBase EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress); }); - await SelectedPackage.InstallPackage(installLocation, progress); + await SelectedPackage.InstallPackage(installLocation, torchVersion, progress); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs index 7edbceaf..8e8e4db3 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs @@ -10,6 +10,7 @@ using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using NLog; +using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper.Factory; @@ -200,6 +201,8 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase "Selected commit is null"); } + var torchVersion = SelectedBasePackage.GetRecommendedTorchVersion(); + var sharedFolderRecommendation = SelectedBasePackage.RecommendedSharedFolderMethod; var package = new InstalledPackage { Id = Guid.NewGuid(), @@ -209,6 +212,8 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase Version = version, LaunchCommand = SelectedBasePackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now, + PreferredTorchVersion = torchVersion, + PreferredSharedFolderMethod = sharedFolderRecommendation }; // Recreate venv if it's a BaseGitPackage @@ -218,7 +223,8 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase } // Reconfigure shared links - await SelectedBasePackage.UpdateModelFolders(PackagePath); + var recommendedSharedFolderMethod = SelectedBasePackage.RecommendedSharedFolderMethod; + await SelectedBasePackage.UpdateModelFolders(PackagePath, recommendedSharedFolderMethod); settingsManager.Transaction(s => s.InstalledPackages.Add(package)); } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs new file mode 100644 index 00000000..86a940cb --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls.Notifications; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.PackageModification; +using StabilityMatrix.Core.Models.Progress; + +namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; + +public class PackageModificationDialogViewModel : ContentDialogProgressViewModelBase +{ + private readonly IPackageModificationRunner packageModificationRunner; + private readonly INotificationService notificationService; + private readonly IEnumerable steps; + + public PackageModificationDialogViewModel(IPackageModificationRunner packageModificationRunner, + INotificationService notificationService, IEnumerable steps) + { + this.packageModificationRunner = packageModificationRunner; + this.notificationService = notificationService; + this.steps = steps; + } + + public ConsoleViewModel Console { get; } = new(); + + public override async Task OnLoadedAsync() + { + // idk why this is getting called twice + if (!packageModificationRunner.IsRunning) + { + packageModificationRunner.ProgressChanged += PackageModificationRunnerOnProgressChanged; + await packageModificationRunner.ExecuteSteps(steps.ToList()); + + notificationService.Show("Package Install Completed", + "Package install completed successfully.", NotificationType.Success); + + OnCloseButtonClick(); + } + } + + private void PackageModificationRunnerOnProgressChanged(object? sender, ProgressReport e) + { + Text = string.IsNullOrWhiteSpace(e.Title) + ? packageModificationRunner.CurrentStep?.ProgressTitle + : e.Title; + + Value = e.Percentage; + Description = e.Message; + IsIndeterminate = e.IsIndeterminate; + + if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Equals("Downloading...")) + return; + + Console.PostLine(e.Message); + EventManager.Instance.OnScrollToBottomRequested(); + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs index 25e8928e..fcdcf647 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs @@ -9,6 +9,7 @@ using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NLog; +using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs index 4d643c26..1c8a5107 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs @@ -9,6 +9,7 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs index 22a6b9ab..7f76be2f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using AsyncAwaitBestPractices; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; diff --git a/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs index 1b8d3131..1ecbca3f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs @@ -233,7 +233,8 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn Console.StartUpdates(); // Update shared folder links (in case library paths changed) - await basePackage.UpdateModelFolders(packagePath); + await basePackage.UpdateModelFolders(packagePath, + activeInstall.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod); // Load user launch args from settings and convert to string var userArgs = settingsManager.GetLaunchArgs(activeInstall.Id); diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs index 2398a06d..257ef71f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs @@ -180,8 +180,11 @@ public partial class PackageCardViewModel : ProgressViewModel EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, packageName, progress)); }); - - var updateResult = await basePackage.Update(Package, progress); + + var torchVersion = Package.PreferredTorchVersion ?? + basePackage.GetRecommendedTorchVersion(); + + var updateResult = await basePackage.Update(Package, torchVersion, progress); settingsManager.UpdatePackageVersionNumber(Package.Id, updateResult); notificationService.Show("Update complete", diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs index ac98e1b6..82c4db59 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs @@ -21,6 +21,7 @@ using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; @@ -37,6 +38,8 @@ public partial class PackageManagerViewModel : PageViewModelBase private readonly ISettingsManager settingsManager; private readonly IPackageFactory packageFactory; private readonly ServiceManager dialogFactory; + private readonly IPackageModificationRunner packageModificationRunner; + private readonly INotificationService notificationService; public override string Title => "Packages"; public override IconSource IconSource => @@ -61,12 +64,16 @@ public partial class PackageManagerViewModel : PageViewModelBase public PackageManagerViewModel( ISettingsManager settingsManager, IPackageFactory packageFactory, - ServiceManager dialogFactory + ServiceManager dialogFactory, + IPackageModificationRunner packageModificationRunner, + INotificationService notificationService ) { this.settingsManager = settingsManager; this.packageFactory = packageFactory; this.dialogFactory = dialogFactory; + this.packageModificationRunner = packageModificationRunner; + this.notificationService = notificationService; EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged; @@ -115,7 +122,7 @@ public partial class PackageManagerViewModel : PageViewModelBase var dialog = new BetterContentDialog { - MaxDialogWidth = 1100, + MaxDialogWidth = 900, MinDialogWidth = 900, DefaultButton = ContentDialogButton.Close, IsPrimaryButtonEnabled = false, @@ -124,7 +131,27 @@ public partial class PackageManagerViewModel : PageViewModelBase Content = new InstallerDialog { DataContext = viewModel } }; - await dialog.ShowAsync(); + var result = await dialog.ShowAsync(); + if (result == ContentDialogResult.Primary) + { + var steps = viewModel.Steps; + var packageModificationDialogViewModel = + new PackageModificationDialogViewModel(packageModificationRunner, notificationService, steps); + + dialog = new BetterContentDialog + { + MaxDialogWidth = 900, + MinDialogWidth = 900, + DefaultButton = ContentDialogButton.Close, + IsPrimaryButtonEnabled = false, + IsSecondaryButtonEnabled = false, + IsFooterVisible = false, + Content = new PackageModificationDialog {DataContext = packageModificationDialogViewModel} + }; + + await dialog.ShowAsync(); + } + await OnLoadedAsync(); } diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml index cdb8e15f..0128c3a7 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml @@ -9,8 +9,6 @@ xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" - xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" - xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" x:DataType="dialogs:InstallerViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700" d:DataContext="{x:Static mocks:DesignData.InstallerViewModel}" @@ -19,18 +17,6 @@ - - - - - - - - - - - - + ColumnDefinitions="Auto, Auto, Auto, Auto"> - - + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Core/Helper/EventManager.cs b/StabilityMatrix.Core/Helper/EventManager.cs index 025f7de5..8c90c373 100644 --- a/StabilityMatrix.Core/Helper/EventManager.cs +++ b/StabilityMatrix.Core/Helper/EventManager.cs @@ -1,4 +1,5 @@ using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Update; @@ -10,11 +11,8 @@ public class EventManager { public static EventManager Instance { get; } = new(); - private EventManager() - { + private EventManager() { } - } - public event EventHandler? GlobalProgressChanged; public event EventHandler? InstalledPackagesChanged; public event EventHandler? OneClickInstallFinished; @@ -24,20 +22,42 @@ public class EventManager public event EventHandler? PackageLaunchRequested; public event EventHandler? ScrollToBottomRequested; public event EventHandler? ProgressChanged; - public event EventHandler? RunningPackageStatusChanged; - - public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress); - public void OnInstalledPackagesChanged() => InstalledPackagesChanged?.Invoke(this, EventArgs.Empty); - public void OnOneClickInstallFinished(bool skipped) => OneClickInstallFinished?.Invoke(this, skipped); + public event EventHandler? RunningPackageStatusChanged; + + public event EventHandler? PackageInstallProgressAdded; + public event EventHandler? ToggleProgressFlyout; + + public void OnGlobalProgressChanged(int progress) => + GlobalProgressChanged?.Invoke(this, progress); + + public void OnInstalledPackagesChanged() => + InstalledPackagesChanged?.Invoke(this, EventArgs.Empty); + + public void OnOneClickInstallFinished(bool skipped) => + OneClickInstallFinished?.Invoke(this, skipped); + public void OnTeachingTooltipNeeded() => TeachingTooltipNeeded?.Invoke(this, EventArgs.Empty); + public void OnDevModeSettingChanged(bool value) => DevModeSettingChanged?.Invoke(this, value); + public void OnUpdateAvailable(UpdateInfo args) => UpdateAvailable?.Invoke(this, args); + public void OnPackageLaunchRequested(Guid packageId) => PackageLaunchRequested?.Invoke(this, packageId); + public void OnScrollToBottomRequested() => ScrollToBottomRequested?.Invoke(this, EventArgs.Empty); - public void OnProgressChanged(ProgressItem progress) => - ProgressChanged?.Invoke(this, progress); + + public void OnProgressChanged(ProgressItem progress) => ProgressChanged?.Invoke(this, progress); + public void OnRunningPackageStatusChanged(PackagePair? currentPackagePair) => - RunningPackageStatusChanged?.Invoke(this, new RunningPackageStatusChangedEventArgs(currentPackagePair)); + RunningPackageStatusChanged?.Invoke( + this, + new RunningPackageStatusChangedEventArgs(currentPackagePair) + ); + + public void OnPackageInstallProgressAdded(IPackageModificationRunner runner) => + PackageInstallProgressAdded?.Invoke(this, runner); + + public void OnToggleProgressFlyout() => ToggleProgressFlyout?.Invoke(this, EventArgs.Empty); } diff --git a/StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs b/StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs index 1b431c16..434e06a0 100644 --- a/StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs +++ b/StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs @@ -19,10 +19,15 @@ public class AddInstalledPackageStep : IPackageStep public async Task ExecuteAsync(IProgress? progress = null) { + if (!string.IsNullOrWhiteSpace(newInstalledPackage.DisplayName)) + { + settingsManager.PackageInstallsInProgress.Remove(newInstalledPackage.DisplayName); + } + await using var transaction = settingsManager.BeginTransaction(); transaction.Settings.InstalledPackages.Add(newInstalledPackage); transaction.Settings.ActiveInstalledPackageId = newInstalledPackage.Id; } - public string ProgressTitle => "Finishing up..."; + public string ProgressTitle => $"{newInstalledPackage.DisplayName} Installed"; } diff --git a/StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs b/StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs index a39dfae2..c5d6d008 100644 --- a/StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs +++ b/StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs @@ -9,4 +9,6 @@ public interface IPackageModificationRunner ProgressReport CurrentProgress { get; set; } IPackageStep? CurrentStep { get; set; } event EventHandler? ProgressChanged; + List ConsoleOutput { get; } + Guid Id { get; } } diff --git a/StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs b/StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs index 0f884fee..48990902 100644 --- a/StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs +++ b/StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs @@ -6,9 +6,14 @@ public class PackageModificationRunner : IPackageModificationRunner { public async Task ExecuteSteps(IReadOnlyList steps) { - var progress = new Progress(report => + IProgress progress = new Progress(report => { CurrentProgress = report; + if (!string.IsNullOrWhiteSpace(report.Message)) + { + ConsoleOutput.Add(report.Message); + } + OnProgressChanged(report); }); @@ -19,12 +24,18 @@ public class PackageModificationRunner : IPackageModificationRunner await step.ExecuteAsync(progress).ConfigureAwait(false); } + progress.Report( + new ProgressReport(1f, message: "Package Install Complete", isIndeterminate: false) + ); + IsRunning = false; } public bool IsRunning { get; set; } public ProgressReport CurrentProgress { get; set; } public IPackageStep? CurrentStep { get; set; } + public List ConsoleOutput { get; } = new(); + public Guid Id { get; } = Guid.NewGuid(); public event EventHandler? ProgressChanged; diff --git a/StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs b/StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs new file mode 100644 index 00000000..9867fd50 --- /dev/null +++ b/StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs @@ -0,0 +1,24 @@ +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Core.Models.PackageModification; + +public class SetPackageInstallingStep : IPackageStep +{ + private readonly ISettingsManager settingsManager; + private readonly string packageName; + + public SetPackageInstallingStep(ISettingsManager settingsManager, string packageName) + { + this.settingsManager = settingsManager; + this.packageName = packageName; + } + + public Task ExecuteAsync(IProgress? progress = null) + { + settingsManager.PackageInstallsInProgress.Add(packageName); + return Task.CompletedTask; + } + + public string ProgressTitle => "Starting Package Installation"; +} diff --git a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs index c363c7c9..e98b367c 100644 --- a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs +++ b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs @@ -101,6 +101,12 @@ public class VladAutomatic : BaseGitPackage Options = new() { "--lowvram", "--medvram" } }, new() + { + Name = "Auto-Launch Web UI", + Type = LaunchOptionType.Bool, + Options = new() { "--autolaunch" } + }, + new() { Name = "Force use of Intel OneAPI XPU backend", Type = LaunchOptionType.Bool, diff --git a/StabilityMatrix.Core/Models/Progress/ProgressItem.cs b/StabilityMatrix.Core/Models/Progress/ProgressItem.cs index 244c7d04..dd3d7551 100644 --- a/StabilityMatrix.Core/Models/Progress/ProgressItem.cs +++ b/StabilityMatrix.Core/Models/Progress/ProgressItem.cs @@ -1,3 +1,8 @@ namespace StabilityMatrix.Core.Models.Progress; -public record ProgressItem(Guid ProgressId, string Name, ProgressReport Progress, bool Failed = false); +public record ProgressItem( + Guid ProgressId, + string Name, + ProgressReport Progress, + bool Failed = false +); diff --git a/StabilityMatrix.Core/Services/ISettingsManager.cs b/StabilityMatrix.Core/Services/ISettingsManager.cs index aec86465..0bd1030a 100644 --- a/StabilityMatrix.Core/Services/ISettingsManager.cs +++ b/StabilityMatrix.Core/Services/ISettingsManager.cs @@ -15,6 +15,7 @@ public interface ISettingsManager string ModelsDirectory { get; } string DownloadsDirectory { get; } Settings Settings { get; } + List PackageInstallsInProgress { get; set; } event EventHandler? LibraryDirChanged; event EventHandler? SettingsPropertyChanged; diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index 076f1034..43fddf82 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -56,6 +56,7 @@ public class SettingsManager : ISettingsManager private string SettingsPath => Path.Combine(LibraryDir, "settings.json"); public string ModelsDirectory => Path.Combine(LibraryDir, "Models"); public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads"); + public List PackageInstallsInProgress { get; set; } = new(); public Settings Settings { get; private set; } = new(); From 28dd51844c282a3e47464772a893dc3af58e77bd Mon Sep 17 00:00:00 2001 From: JT Date: Wed, 6 Sep 2023 23:32:13 -0700 Subject: [PATCH 10/29] Forgot the stupid task file --- .husky/task-runner.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/task-runner.json b/.husky/task-runner.json index 8201d1d3..cedfb34b 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -11,7 +11,7 @@ "name": "Run xamlstyler", "group": "pre-commit", "command": "dotnet", - "args": [ "xstyler", "${staged}" ], + "args": [ "xstyler", "-f", "${staged}" ], "include": [ "**/*.axaml" ] } ] From 3f53bc16090e3b6e2e408fb075c0fe416677dc91 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 7 Sep 2023 00:33:55 -0700 Subject: [PATCH 11/29] Fix merge weirdness --- .config/dotnet-tools.json | 8 +- .github/workflows/release.yml | 23 +-- CHANGELOG.md | 11 +- StabilityMatrix.Avalonia/Assets.cs | 146 ++++++++++++------ StabilityMatrix.Avalonia/Program.cs | 118 ++++++++------ .../ViewModels/Dialogs/UpdateViewModel.cs | 11 +- 6 files changed, 205 insertions(+), 112 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b1761c61..5b4bb470 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -13,6 +13,12 @@ "commands": [ "xstyler" ] + }, + "csharpier": { + "version": "0.25.0", + "commands": [ + "dotnet-csharpier" + ] } } -} \ No newline at end of file +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80287394..80d6cdd1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,8 @@ name: Release +permissions: + contents: write + on: workflow_dispatch: inputs: @@ -82,20 +85,7 @@ jobs: path: ${{ env.out-name }} - name: Create Sentry release - if: ${{ github.event_name == 'release' || github.event.inputs.sentry-release == 'true' }} - uses: getsentry/action-release@v1 - env: - MAKE_SENTRY_RELEASE: ${{ secrets.SENTRY_PROJECT != '' }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ secrets.SENTRY_ORG }} - SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - with: - environment: production - ignore_missing: true - version: StabilityMatrix.Avalonia@${{ env.GIT_TAG_NAME }} - - - name: Create Sentry release - if: ${{ github.event_name == 'workflow_dispatch' }} + if: ${{ github.event.inputs.sentry-release == 'true' }} uses: getsentry/action-release@v1 env: MAKE_SENTRY_RELEASE: ${{ secrets.SENTRY_PROJECT != '' }} @@ -105,8 +95,9 @@ jobs: with: environment: production ignore_missing: true + ignore_empty: true version: StabilityMatrix.Avalonia@${{ github.event.inputs.version }} - + release-windows: name: Release (win-x64) @@ -206,4 +197,4 @@ jobs: tag_name: v${{ github.event.inputs.version }} body: ${{ steps.release_notes.outputs.release_notes }} draft: ${{ github.event.inputs.github-release-draft == 'true' }} - prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }} + prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b32fcb..f764e7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,16 @@ 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.4 +## v2.4.0 ### Changed -- Revamped Package Installer dialog with some more advanced options +- Revamped package installer + - Added "advanced options" section for commit, shared folder method, and pytorch options + - Can be run in the background + - Shows progress in the Downloads tab + +## v2.3.4 +### Fixed +- Fixed [#108](https://github.com/LykosAI/StabilityMatrix/issues/108) - (Linux) Fixed permission error on updates [#103](https://github.com/LykosAI/StabilityMatrix/pull/103) ## v2.3.3 ### Fixed diff --git a/StabilityMatrix.Avalonia/Assets.cs b/StabilityMatrix.Avalonia/Assets.cs index 8e11c868..6b06b2ea 100644 --- a/StabilityMatrix.Avalonia/Assets.cs +++ b/StabilityMatrix.Avalonia/Assets.cs @@ -12,73 +12,133 @@ internal static class Assets { public static AvaloniaResource AppIcon { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico"); - + public static AvaloniaResource AppIconPng { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.png"); - + /// /// Fixed image for models with no images. /// public static Uri NoImage { get; } = new("avares://StabilityMatrix.Avalonia/Assets/noimage.png"); - - public static AvaloniaResource LicensesJson => new( - "avares://StabilityMatrix.Avalonia/Assets/licenses.json"); + + public static AvaloniaResource LicensesJson => + new("avares://StabilityMatrix.Avalonia/Assets/licenses.json"); + + private const UnixFileMode unix755 = + UnixFileMode.UserRead + | UnixFileMode.UserWrite + | UnixFileMode.UserExecute + | UnixFileMode.GroupRead + | UnixFileMode.GroupExecute + | UnixFileMode.OtherRead + | UnixFileMode.OtherExecute; [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) 0777)), - (PlatformKind.MacOS | PlatformKind.Arm, - new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", (UnixFileMode) 0x777))); - + 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", + unix755 + ) + ), + ( + PlatformKind.MacOS | PlatformKind.Arm, + new AvaloniaResource( + "avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", + unix755 + ) + ) + ); + [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.MacOS | PlatformKind.Arm, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt"))); + 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.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"); - public static AvaloniaResource PyScriptSiteCustomize => new( - "avares://StabilityMatrix.Avalonia/Assets/sitecustomize.py"); - [SupportedOSPlatform("windows")] - public static AvaloniaResource PyScriptGetPip => new( - "avares://StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc"); - + public static AvaloniaResource PyScriptGetPip => + new("avares://StabilityMatrix.Avalonia/Assets/win-x64/get-pip.pyc"); + [SupportedOSPlatform("windows")] - public static IEnumerable<(AvaloniaResource resource, string relativePath)> PyModuleVenv => + public static IEnumerable<(AvaloniaResource resource, string relativePath)> PyModuleVenv => FindAssets("win-x64/venv/"); - + [SupportedOSPlatform("windows")] [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] - public static RemoteResource PythonDownloadUrl => Compat.Switch( - (PlatformKind.Windows | PlatformKind.X64, new RemoteResource( - new Uri("https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"), - "608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d")), - (PlatformKind.Linux | PlatformKind.X64, new RemoteResource( - new Uri("https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz"), - "c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79")), - (PlatformKind.MacOS | PlatformKind.Arm, new RemoteResource( - new Uri("https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz"), - "8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f"))); - - public static Uri DiscordServerUrl { get; } = - new("https://discord.com/invite/TUrgfECxHz"); - - public static Uri PatreonUrl { get; } = - new("https://patreon.com/StabilityMatrix"); - + public static RemoteResource PythonDownloadUrl => + Compat.Switch( + ( + PlatformKind.Windows | PlatformKind.X64, + new RemoteResource( + new Uri( + "https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip" + ), + "608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d" + ) + ), + ( + PlatformKind.Linux | PlatformKind.X64, + new RemoteResource( + new Uri( + "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz" + ), + "c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79" + ) + ), + ( + PlatformKind.MacOS | PlatformKind.Arm, + new RemoteResource( + new Uri( + "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz" + ), + "8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f" + ) + ) + ); + + public static Uri DiscordServerUrl { get; } = new("https://discord.com/invite/TUrgfECxHz"); + + public static Uri PatreonUrl { get; } = new("https://patreon.com/StabilityMatrix"); + /// /// Yield AvaloniaResources given a relative directory path within the 'Assets' folder. /// - public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(string relativeAssetPath) + public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets( + string relativeAssetPath + ) { var baseUri = new Uri("avares://StabilityMatrix.Avalonia/Assets/"); var targetUri = new Uri(baseUri, relativeAssetPath); diff --git a/StabilityMatrix.Avalonia/Program.cs b/StabilityMatrix.Avalonia/Program.cs index c693b224..b0021802 100644 --- a/StabilityMatrix.Avalonia/Program.cs +++ b/StabilityMatrix.Avalonia/Program.cs @@ -31,9 +31,9 @@ namespace StabilityMatrix.Avalonia; public class Program { public static AppArgs Args { get; } = new(); - + public static bool IsDebugBuild { get; private set; } - + // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. @@ -47,28 +47,30 @@ public class Program Args.ResetWindowPosition = args.Contains("--reset-window-position"); SetDebugBuild(); - + HandleUpdateReplacement(); - - var infoVersion = Assembly.GetExecutingAssembly() - .GetCustomAttribute()?.InformationalVersion; + + var infoVersion = Assembly + .GetExecutingAssembly() + .GetCustomAttribute() + ?.InformationalVersion; Compat.AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict); - + // Configure exception dialog for unhandled exceptions if (!Debugger.IsAttached || Args.DebugExceptionDialog) { AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; } - + // Configure Sentry if (!Args.NoSentry && (!Debugger.IsAttached || Args.DebugSentry)) { ConfigureSentry(); } - + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } - + // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() { @@ -76,24 +78,23 @@ public class Program // Use our custom image loader for custom local load error handling ImageLoader.AsyncImageLoader.Dispose(); ImageLoader.AsyncImageLoader = new FallbackRamCachedWebImageLoader(); - - return AppBuilder.Configure() - .UsePlatformDetect() - .WithInterFont() - .LogToTrace(); + + return AppBuilder.Configure().UsePlatformDetect().WithInterFont().LogToTrace(); } private static void HandleUpdateReplacement() { // Check if we're in the named update folder or the legacy update folder for 1.2.0 -> 2.0.0 - if (Compat.AppCurrentDir is {Name: UpdateHelper.UpdateFolderName} or {Name: "Update"}) + if (Compat.AppCurrentDir is { Name: UpdateHelper.UpdateFolderName } or { Name: "Update" }) { var parentDir = Compat.AppCurrentDir.Parent; - if (parentDir is null) + if (parentDir is null) return; - + var retryDelays = Backoff.DecorrelatedJitterBackoffV2( - TimeSpan.FromMilliseconds(350), retryCount: 5); + TimeSpan.FromMilliseconds(350), + retryCount: 5 + ); foreach (var delay in retryDelays) { @@ -103,16 +104,25 @@ public class Program try { currentExe.CopyTo(targetExe, true); - + // Ensure permissions are set for unix if (Compat.IsUnix) { - File.SetUnixFileMode(targetExe, (UnixFileMode) 0x755); + File.SetUnixFileMode( + targetExe, // 0755 + UnixFileMode.UserRead + | UnixFileMode.UserWrite + | UnixFileMode.UserExecute + | UnixFileMode.GroupRead + | UnixFileMode.GroupExecute + | UnixFileMode.OtherRead + | UnixFileMode.OtherExecute + ); } - + // Start the new app Process.Start(targetExe); - + // Shutdown the current app Environment.Exit(0); } @@ -122,7 +132,7 @@ public class Program } } } - + // Delete update folder if it exists in current directory var updateDir = UpdateHelper.UpdateFolder; if (updateDir.Exists) @@ -138,12 +148,13 @@ public class Program } } } - + private static void ConfigureSentry() { SentrySdk.Init(o => { - o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328"; + o.Dsn = + "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328"; o.StackTraceMode = StackTraceMode.Enhanced; o.TracesSampleRate = 1.0; o.IsGlobalModeEnabled = true; @@ -156,47 +167,56 @@ public class Program #endif }); } - - private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + + private static void CurrentDomain_UnhandledException( + object sender, + UnhandledExceptionEventArgs e + ) { - if (e.ExceptionObject is not Exception ex) return; - + if (e.ExceptionObject is not Exception ex) + return; + var logger = LogManager.GetCurrentClassLogger(); logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message); - + if (SentrySdk.IsEnabled) { SentrySdk.CaptureException(ex); } - - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + + if ( + Application.Current?.ApplicationLifetime + is IClassicDesktopStyleApplicationLifetime lifetime + ) { var dialog = new ExceptionDialog { - DataContext = new ExceptionViewModel - { - Exception = ex - } + DataContext = new ExceptionViewModel { Exception = ex } }; - + var mainWindow = lifetime.MainWindow; // We can only show dialog if main window exists, and is visible - if (mainWindow is {PlatformImpl: not null, IsVisible: true}) + if (mainWindow is { PlatformImpl: not null, IsVisible: true }) { // Configure for dialog mode dialog.ShowAsDialog = true; dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner; - + // Show synchronously without blocking UI thread // https://github.com/AvaloniaUI/Avalonia/issues/4810#issuecomment-704259221 var cts = new CancellationTokenSource(); - - dialog.ShowDialog(mainWindow).ContinueWith(_ => - { - cts.Cancel(); - ExitWithException(ex); - }, TaskScheduler.FromCurrentSynchronizationContext()); - + + dialog + .ShowDialog(mainWindow) + .ContinueWith( + _ => + { + cts.Cancel(); + ExitWithException(ex); + }, + TaskScheduler.FromCurrentSynchronizationContext() + ); + Dispatcher.UIThread.MainLoop(cts.Token); } else @@ -205,9 +225,9 @@ public class Program var cts = new CancellationTokenSource(); // Exit on token cancellation cts.Token.Register(() => ExitWithException(ex)); - + dialog.ShowWithCts(cts); - + Dispatcher.UIThread.MainLoop(cts.Token); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs index 4eb562a8..3c7cfef0 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs @@ -114,7 +114,16 @@ public partial class UpdateViewModel : ContentDialogViewModelBase // On unix, we need to set the executable bit if (Compat.IsUnix) { - File.SetUnixFileMode(UpdateHelper.ExecutablePath, (UnixFileMode)0x755); + File.SetUnixFileMode( + UpdateHelper.ExecutablePath, // 0755 + UnixFileMode.UserRead + | UnixFileMode.UserWrite + | UnixFileMode.UserExecute + | UnixFileMode.GroupRead + | UnixFileMode.GroupExecute + | UnixFileMode.OtherRead + | UnixFileMode.OtherExecute + ); } UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds..."; From 2d6fa76383117dc94ac92b64fc6c878bfb6b7448 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 7 Sep 2023 00:34:34 -0700 Subject: [PATCH 12/29] add back csharpierrc --- .config/.csharpierrc.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .config/.csharpierrc.json diff --git a/.config/.csharpierrc.json b/.config/.csharpierrc.json new file mode 100644 index 00000000..c821bbeb --- /dev/null +++ b/.config/.csharpierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "preprocessorSymbolSets": ["", "DEBUG", "DEBUG,CODE_STYLE"] +} From ce85dc1f502c2ace341423e19d50d8a22b729014 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 7 Sep 2023 00:41:11 -0700 Subject: [PATCH 13/29] Remove package name lookup thing --- .../Dialogs/PackageModificationDialogViewModel.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs index c2a65a4d..40acfa52 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs @@ -41,16 +41,6 @@ public class PackageModificationDialogViewModel : ContentDialogProgressViewModel packageModificationRunner.ProgressChanged += PackageModificationRunnerOnProgressChanged; await packageModificationRunner.ExecuteSteps(steps.ToList()); - var packageName = string.Empty; - var addPackageStep = steps.FirstOrDefault(step => step is AddInstalledPackageStep); - if (addPackageStep != null) - { - addPackageStep - .GetType() - .GetProperty("newInstalledPackage") - ?.GetValue(addPackageStep, null); - } - notificationService.Show( "Package Install Completed", "Package install completed successfully.", From ff240ca1d09dc97cba97dd66f436098b0d81b656 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 7 Sep 2023 17:38:54 -0700 Subject: [PATCH 14/29] Remove PackageModificationDialogViewModel & call Console.StartUpdates --- StabilityMatrix.Avalonia/App.axaml.cs | 3 + .../PackageModificationDialogViewModel.cs | 75 ------------------- .../ViewModels/PackageManagerViewModel.cs | 33 +++----- .../PackageInstallProgressItemViewModel.cs | 8 +- .../Progress/ProgressManagerViewModel.cs | 53 +++++++++---- .../Views/Dialogs/InstallerDialog.axaml | 1 + .../Dialogs/PackageModificationDialog.axaml | 1 + 7 files changed, 57 insertions(+), 117 deletions(-) delete mode 100644 StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 57c82ddb..5e37768d 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -605,6 +605,9 @@ public sealed class App : Application builder.ForLogger("System.*").WriteToNil(NLog.LogLevel.Warn); builder.ForLogger("Microsoft.*").WriteToNil(NLog.LogLevel.Warn); builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn); + builder + .ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel") + .WriteToNil(NLog.LogLevel.Debug); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget); diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs deleted file mode 100644 index 40acfa52..00000000 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageModificationDialogViewModel.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Avalonia.Controls.Notifications; -using StabilityMatrix.Avalonia.Services; -using StabilityMatrix.Avalonia.ViewModels.Base; -using StabilityMatrix.Core.Helper; -using StabilityMatrix.Core.Models.PackageModification; -using StabilityMatrix.Core.Models.Progress; - -namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; - -public class PackageModificationDialogViewModel : ContentDialogProgressViewModelBase -{ - private readonly IPackageModificationRunner packageModificationRunner; - private readonly INotificationService notificationService; - private readonly IEnumerable steps; - - public PackageModificationDialogViewModel( - IPackageModificationRunner packageModificationRunner, - INotificationService notificationService, - IEnumerable steps - ) - { - this.packageModificationRunner = packageModificationRunner; - this.notificationService = notificationService; - this.steps = steps; - CloseWhenFinished = true; - } - - public override async Task OnLoadedAsync() - { - await Console.Clear(); - Console.Post(string.Join(Environment.NewLine, packageModificationRunner.ConsoleOutput)); - - // idk why this is getting called twice - if (!packageModificationRunner.IsRunning) - { - EventManager.Instance.OnPackageInstallProgressAdded(packageModificationRunner); - packageModificationRunner.ProgressChanged += PackageModificationRunnerOnProgressChanged; - await packageModificationRunner.ExecuteSteps(steps.ToList()); - - notificationService.Show( - "Package Install Completed", - "Package install completed successfully.", - NotificationType.Success - ); - - EventManager.Instance.OnInstalledPackagesChanged(); - - if (CloseWhenFinished) - { - OnCloseButtonClick(); - } - } - } - - private void PackageModificationRunnerOnProgressChanged(object? sender, ProgressReport e) - { - Text = string.IsNullOrWhiteSpace(e.Title) - ? packageModificationRunner.CurrentStep?.ProgressTitle - : e.Title; - - Value = e.Percentage; - Description = e.Message; - IsIndeterminate = e.IsIndeterminate; - - if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Equals("Downloading...")) - return; - - Console.PostLine(e.Message); - EventManager.Instance.OnScrollToBottomRequested(); - } -} diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs index 10d937a0..5007bef5 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; +using Avalonia.Controls.Notifications; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; @@ -38,7 +39,6 @@ public partial class PackageManagerViewModel : PageViewModelBase private readonly ISettingsManager settingsManager; private readonly IPackageFactory packageFactory; private readonly ServiceManager dialogFactory; - private readonly IPackageModificationRunner packageModificationRunner; private readonly INotificationService notificationService; public override string Title => "Packages"; @@ -65,14 +65,12 @@ public partial class PackageManagerViewModel : PageViewModelBase ISettingsManager settingsManager, IPackageFactory packageFactory, ServiceManager dialogFactory, - IPackageModificationRunner packageModificationRunner, INotificationService notificationService ) { this.settingsManager = settingsManager; this.packageFactory = packageFactory; this.dialogFactory = dialogFactory; - this.packageModificationRunner = packageModificationRunner; this.notificationService = notificationService; EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged; @@ -140,28 +138,17 @@ public partial class PackageManagerViewModel : PageViewModelBase var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { + var runner = new PackageModificationRunner(); var steps = viewModel.Steps; - var packageModificationDialogViewModel = new PackageModificationDialogViewModel( - packageModificationRunner, - notificationService, - steps - ); - dialog = new BetterContentDialog - { - MaxDialogWidth = 900, - MinDialogWidth = 900, - DefaultButton = ContentDialogButton.Close, - IsPrimaryButtonEnabled = false, - IsSecondaryButtonEnabled = false, - IsFooterVisible = false, - Content = new PackageModificationDialog - { - DataContext = packageModificationDialogViewModel - } - }; - - await dialog.ShowAsync(); + EventManager.Instance.OnPackageInstallProgressAdded(runner); + await runner.ExecuteSteps(steps.ToList()); + EventManager.Instance.OnInstalledPackagesChanged(); + notificationService.Show( + "Package Install Complete", + $"{viewModel.InstallName} installed successfully", + NotificationType.Success + ); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs index 1422a7f3..c4b5f845 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Avalonia.Threading; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -25,6 +26,8 @@ public partial class PackageInstallProgressItemViewModel : ProgressItemViewModel Progress.Text = packageModificationRunner.ConsoleOutput.LastOrDefault(); Progress.IsIndeterminate = packageModificationRunner.CurrentProgress.IsIndeterminate; + Progress.Console.StartUpdates(); + Progress.Console.Post( string.Join(Environment.NewLine, packageModificationRunner.ConsoleOutput) ); @@ -39,7 +42,7 @@ public partial class PackageInstallProgressItemViewModel : ProgressItemViewModel Progress.IsIndeterminate = e.IsIndeterminate; Name = packageModificationRunner.CurrentStep?.ProgressTitle; - if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Equals("Downloading...")) + if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Contains("Downloading...")) return; Progress.Console.PostLine(e.Message); @@ -51,8 +54,7 @@ public partial class PackageInstallProgressItemViewModel : ProgressItemViewModel && Progress.CloseWhenFinished ) { - EventManager.Instance.OnInstalledPackagesChanged(); - dialog?.Hide(); + Dispatcher.UIThread.Post(() => dialog?.Hide()); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs index 506d3126..1ede1406 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; using Avalonia.Collections; using Avalonia.Controls.Notifications; using Avalonia.Threading; @@ -23,45 +25,55 @@ namespace StabilityMatrix.Avalonia.ViewModels.Progress; public partial class ProgressManagerViewModel : PageViewModelBase { private readonly INotificationService notificationService; - + public override string Title => "Download Manager"; - public override IconSource IconSource => new SymbolIconSource {Symbol = Symbol.ArrowCircleDown, IsFilled = true}; + public override IconSource IconSource => + new SymbolIconSource { Symbol = Symbol.ArrowCircleDown, IsFilled = true }; public AvaloniaList ProgressItems { get; } = new(); - [ObservableProperty] private bool isOpen; + [ObservableProperty] + private bool isOpen; public ProgressManagerViewModel( ITrackedDownloadService trackedDownloadService, - INotificationService notificationService) + INotificationService notificationService + ) { this.notificationService = notificationService; - + // Attach to the event trackedDownloadService.DownloadAdded += TrackedDownloadService_OnDownloadAdded; EventManager.Instance.PackageInstallProgressAdded += InstanceOnPackageInstallProgressAdded; EventManager.Instance.ToggleProgressFlyout += (_, _) => IsOpen = !IsOpen; } - private void InstanceOnPackageInstallProgressAdded(object? sender, IPackageModificationRunner runner) + private void InstanceOnPackageInstallProgressAdded( + object? sender, + IPackageModificationRunner runner + ) { - AddPackageInstall(runner); + AddPackageInstall(runner).SafeFireAndForget(); } private void TrackedDownloadService_OnDownloadAdded(object? sender, TrackedDownload e) { var vm = new DownloadProgressItemViewModel(e); - + // Attach notification handlers e.ProgressStateChanged += (s, state) => { var download = s as TrackedDownload; - + switch (state) { case ProgressState.Success: Dispatcher.UIThread.Post(() => { - notificationService.Show("Download Completed", $"Download of {e.FileName} completed successfully.", NotificationType.Success); + notificationService.Show( + "Download Completed", + $"Download of {e.FileName} completed successfully.", + NotificationType.Success + ); }); break; case ProgressState.Failed: @@ -72,18 +84,26 @@ public partial class ProgressManagerViewModel : PageViewModelBase } Dispatcher.UIThread.Post(() => { - notificationService.ShowPersistent("Download Failed", $"Download of {e.FileName} failed: {msg}", NotificationType.Error); + notificationService.ShowPersistent( + "Download Failed", + $"Download of {e.FileName} failed: {msg}", + NotificationType.Error + ); }); break; case ProgressState.Cancelled: Dispatcher.UIThread.Post(() => { - notificationService.Show("Download Cancelled", $"Download of {e.FileName} was cancelled.", NotificationType.Warning); + notificationService.Show( + "Download Cancelled", + $"Download of {e.FileName} was cancelled.", + NotificationType.Warning + ); }); break; } }; - + ProgressItems.Add(vm); } @@ -100,7 +120,7 @@ public partial class ProgressManagerViewModel : PageViewModelBase } } - private void AddPackageInstall(IPackageModificationRunner packageModificationRunner) + private async Task AddPackageInstall(IPackageModificationRunner packageModificationRunner) { if (ProgressItems.Any(vm => vm.Id == packageModificationRunner.Id)) { @@ -109,13 +129,14 @@ public partial class ProgressManagerViewModel : PageViewModelBase var vm = new PackageInstallProgressItemViewModel(packageModificationRunner); ProgressItems.Add(vm); + await vm.ShowProgressDialog(); } - + private void ShowFailedNotification(string title, string message) { notificationService.ShowPersistent(title, message, NotificationType.Error); } - + public void StartEventListener() { EventManager.Instance.ProgressChanged += OnProgressChanged; diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml index 0128c3a7..261111ac 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml @@ -197,6 +197,7 @@ Content="Install" Command="{Binding InstallCommand}" FontSize="20" + IsEnabled="{Binding !ShowDuplicateWarning}" HorizontalAlignment="Center" Classes="success" Margin="4,0,8,0" diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml index fabaefd7..aa6c4018 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml @@ -12,6 +12,7 @@ From 16299d46393fa563d25773cf87a387816d674280 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 7 Sep 2023 20:39:52 -0400 Subject: [PATCH 15/29] Use conditional logger for ConsoleViewModel --- .../ViewModels/ConsoleViewModel.cs | 199 +++++++++++------- 1 file changed, 119 insertions(+), 80 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs index c957f9a4..b4cb1283 100644 --- a/StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs @@ -20,18 +20,21 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private bool isDisposed; - + // Queue for console updates private BufferBlock buffer = new(); + // Task that updates the console (runs on UI thread) private Task? updateTask; + // Cancellation token source for updateTask private CancellationTokenSource? updateCts; - + public bool IsUpdatesRunning => updateTask?.IsCompleted == false; - - [ObservableProperty] private TextDocument document = new(); - + + [ObservableProperty] + private TextDocument document = new(); + /// /// Current offset for write operations. /// @@ -47,13 +50,13 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis /// // ReSharper disable once MemberCanBePrivate.Global public TimeSpan WriteCursorLockTimeout { get; init; } = TimeSpan.FromMilliseconds(100); - + /// /// Gets a cancellation token using the cursor lock timeout /// - private CancellationToken WriteCursorLockTimeoutToken => + private CancellationToken WriteCursorLockTimeoutToken => new CancellationTokenSource(WriteCursorLockTimeout).Token; - + /// /// Event invoked when an ApcMessage of type Input is received. /// @@ -72,7 +75,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis updateCts = new CancellationTokenSource(); updateTask = Dispatcher.UIThread.InvokeAsync(ConsoleUpdateLoop, DispatcherPriority.Send); } - + /// /// Cancels the update task and waits for it to complete. /// @@ -97,7 +100,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis // Cancel update task updateCts?.Cancel(); updateCts = null; - + // Wait for update task if (updateTask is not null) { @@ -106,7 +109,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis } Logger.Trace($"Stopped console updates with {buffer.Count} buffer items remaining"); } - + /// /// Clears the console and sets a new buffer. /// This also resets the write cursor to 0. @@ -121,7 +124,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis buffer.Complete(); buffer = new BufferBlock(); } - + /// /// Resets the write cursor to be equal to the document length. /// @@ -129,20 +132,23 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis { using (await writeCursorLock.LockAsync(WriteCursorLockTimeoutToken)) { - Debug.WriteLine($"Reset cursor to end: ({writeCursor} -> {Document.TextLength})"); + Logger.ConditionalTrace( + $"Reset cursor to end: ({writeCursor} -> {Document.TextLength})" + ); writeCursor = Document.TextLength; } DebugPrintDocument(); } - + private async Task ConsoleUpdateLoop() { // This must be run in the UI thread Dispatcher.UIThread.VerifyAccess(); // Get cancellation token - var ct = updateCts?.Token - ?? throw new InvalidOperationException("Update cancellation token must be set"); + var ct = + updateCts?.Token + ?? throw new InvalidOperationException("Update cancellation token must be set"); try { @@ -161,16 +167,19 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis } var outputType = output.IsStdErr ? "stderr" : "stdout"; - Debug.WriteLine($"Processing: [{outputType}] (Text = {output.Text.ToRepr()}, " + - $"Raw = {output.RawText?.ToRepr()}, " + - $"CarriageReturn = {output.CarriageReturn}, " + - $"CursorUp = {output.CursorUp}, " + - $"AnsiCommand = {output.AnsiCommand})"); - + Logger.ConditionalTrace( + $"Processing: [{outputType}] (Text = {output.Text.ToRepr()}, " + + $"Raw = {output.RawText?.ToRepr()}, " + + $"CarriageReturn = {output.CarriageReturn}, " + + $"CursorUp = {output.CursorUp}, " + + $"AnsiCommand = {output.AnsiCommand})" + ); + // Link the cancellation token to the write cursor lock timeout var linkedCt = CancellationTokenSource - .CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken).Token; - + .CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken) + .Token; + using (await writeCursorLock.LockAsync(linkedCt)) { ConsoleUpdateOne(output); @@ -184,7 +193,10 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis catch (Exception e) { // Log other errors and continue here to not crash the UI thread - Logger.Error(e, $"Unexpected error in console update loop: {e.GetType().Name} {e.Message}"); + Logger.Error( + e, + $"Unexpected error in console update loop: {e.GetType().Name} {e.Message}" + ); } } @@ -196,7 +208,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis private void ConsoleUpdateOne(ProcessOutput output) { Debug.Assert(Dispatcher.UIThread.CheckAccess()); - + // Check for Apc messages if (output.ApcMessage is not null) { @@ -209,21 +221,23 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis // Ignore further processing return; } - + // If we have a carriage return, // start current write at the beginning of the current line if (output.CarriageReturn > 0) { var currentLine = Document.GetLineByOffset(writeCursor); - + // Get the start of current line as new write cursor var lineStartOffset = currentLine.Offset; - + // See if we need to move the cursor if (lineStartOffset == writeCursor) { - Debug.WriteLine($"Cursor already at start for carriage return " + - $"(offset = {lineStartOffset}, line = {currentLine.LineNumber})"); + Logger.ConditionalTrace( + $"Cursor already at start for carriage return " + + $"(offset = {lineStartOffset}, line = {currentLine.LineNumber})" + ); } else { @@ -232,19 +246,21 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis var lineEndOffset = currentLine.EndOffset; var lineLength = lineEndOffset - lineStartOffset; Document.Remove(lineStartOffset, lineLength); - - Debug.WriteLine($"Moving cursor to start for carriage return " + - $"({writeCursor} -> {lineStartOffset})"); + + Logger.ConditionalTrace( + $"Moving cursor to start for carriage return " + + $"({writeCursor} -> {lineStartOffset})" + ); writeCursor = lineStartOffset; } } - + // Write new text if (!string.IsNullOrEmpty(output.Text)) { DirectWriteLinesToConsole(output.Text); } - + // Handle cursor movements if (output.CursorUp > 0) { @@ -254,22 +270,27 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis if (currentLocation.Line == 1) { // We are already on the first line, ignore - Debug.WriteLine($"Cursor up: Already on first line"); + Logger.ConditionalTrace($"Cursor up: Already on first line"); } else { // We want to move up one line - var targetLocation = new TextLocation(currentLocation.Line - 1, currentLocation.Column); + var targetLocation = new TextLocation( + currentLocation.Line - 1, + currentLocation.Column + ); var targetOffset = Document.GetOffset(targetLocation); // Update cursor to target offset - Debug.WriteLine($"Cursor up: Moving (line {currentLocation.Line}, {writeCursor})" + - $" -> (line {targetLocation.Line}, {targetOffset})"); + Logger.ConditionalTrace( + $"Cursor up: Moving (line {currentLocation.Line}, {writeCursor})" + + $" -> (line {targetLocation.Line}, {targetOffset})" + ); writeCursor = targetOffset; } } - + // Handle erase commands, different to cursor move as they don't move the cursor // We'll insert blank spaces instead if (output.AnsiCommand.HasFlag(AnsiCommand.EraseLine)) @@ -278,9 +299,11 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis var currentLine = Document.GetLineByOffset(writeCursor); // Make some spaces to insert var spaces = new string(' ', currentLine.Length); - + // Insert the text - Debug.WriteLine($"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})"); + Logger.ConditionalTrace( + $"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})" + ); using (Document.RunUpdate()) { Document.Replace(currentLine.Offset, currentLine.Length, spaces); @@ -289,7 +312,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis DebugPrintDocument(); } - + /// /// Write text potentially containing line breaks to the console. /// This call will hold a upgradeable read lock @@ -298,7 +321,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis { // When our cursor is not at end, newlines should be interpreted as commands to // move cursor forward to the next linebreak instead of inserting a newline. - + // If text contains no newlines, we can just call DirectWriteToConsole // Also if cursor is equal to document length if (!text.Contains(Environment.NewLine) || writeCursor == Document.TextLength) @@ -306,7 +329,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis DirectWriteToConsole(text); return; } - + // Otherwise we need to handle how linebreaks are treated // Split text into lines var lines = text.Split(Environment.NewLine).ToList(); @@ -315,29 +338,32 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis { // Insert text DirectWriteToConsole(lineText); - + // Set cursor to start of next line, if we're not already there var currentLine = Document.GetLineByOffset(writeCursor); // If next line is available, move cursor to start of next line if (currentLine.LineNumber < Document.LineCount) { var nextLine = Document.GetLineByNumber(currentLine.LineNumber + 1); - Debug.WriteLine($"Moving cursor to start of next line " + - $"({writeCursor} -> {nextLine.Offset})"); + Logger.ConditionalTrace( + $"Moving cursor to start of next line " + + $"({writeCursor} -> {nextLine.Offset})" + ); writeCursor = nextLine.Offset; } else { // Otherwise move to end of current line, and direct insert a newline var lineEndOffset = currentLine.EndOffset; - Debug.WriteLine($"Moving cursor to end of current line " + - $"({writeCursor} -> {lineEndOffset})"); + Logger.ConditionalTrace( + $"Moving cursor to end of current line " + $"({writeCursor} -> {lineEndOffset})" + ); writeCursor = lineEndOffset; DirectWriteToConsole(Environment.NewLine); } } } - + /// /// Write text to the console, does not handle newlines. /// This should probably only be used by @@ -352,10 +378,11 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis if (replaceLength > 0) { var newText = text[..replaceLength]; - Debug.WriteLine( - $"Replacing: (cursor = {writeCursor}, length = {replaceLength}, " + - $"text = {Document.GetText(writeCursor, replaceLength).ToRepr()} " + - $"-> {newText.ToRepr()})"); + Logger.ConditionalTrace( + $"Replacing: (cursor = {writeCursor}, length = {replaceLength}, " + + $"text = {Document.GetText(writeCursor, replaceLength).ToRepr()} " + + $"-> {newText.ToRepr()})" + ); Document.Replace(writeCursor, replaceLength, newText); writeCursor += replaceLength; @@ -366,8 +393,9 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis if (remainingLength > 0) { var textToInsert = text[replaceLength..]; - Debug.WriteLine($"Inserting: (cursor = {writeCursor}, " + - $"text = {textToInsert.ToRepr()})"); + Logger.ConditionalTrace( + $"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})" + ); Document.Insert(writeCursor, textToInsert); writeCursor += textToInsert.Length; @@ -382,6 +410,9 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis [Conditional("DEBUG")] private void DebugPrintDocument() { + if (!Logger.IsTraceEnabled) + return; + var text = Document.Text; // Add a number for each line // Add an arrow line for where the cursor is, for example (cursor on offset 3): @@ -390,7 +421,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis // ~~~~~~~^ (3) // 2 | This is the second line // - + var lines = text.Split(Environment.NewLine).ToList(); var numberPadding = lines.Count.ToString().Length; for (var i = 0; i < lines.Count; i++) @@ -399,11 +430,11 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis } var cursorLine = Document.GetLineByOffset(writeCursor); var cursorLineOffset = writeCursor - cursorLine.Offset; - + // Need to account for padding + line number + space + cursor line offset var linePadding = numberPadding + 3 + cursorLineOffset; var cursorLineArrow = new string('~', linePadding) + $"^ ({writeCursor})"; - + // If more than line count, append to end if (cursorLine.LineNumber >= lines.Count) { @@ -413,13 +444,13 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis { lines.Insert(cursorLine.LineNumber, cursorLineArrow); } - + var textWithCursor = string.Join(Environment.NewLine, lines); - - Debug.WriteLine("[Current Document]"); - Debug.WriteLine(textWithCursor); + + Logger.ConditionalTrace("[Current Document]"); + Logger.ConditionalTrace(textWithCursor); } - + /// /// Posts an update to the console /// Safe to call on non-UI threads @@ -433,9 +464,10 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis return; } // Otherwise, use manual update one + Logger.Debug("Synchronous post update to console: {@Output}", output); Dispatcher.UIThread.Post(() => ConsoleUpdateOne(output)); } - + /// /// Posts an update to the console. /// Helper for calling Post(ProcessOutput) with strings @@ -444,7 +476,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis { Post(new ProcessOutput { Text = text }); } - + /// /// Posts an update to the console. /// Equivalent to Post(text + Environment.NewLine) @@ -456,20 +488,22 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis public void Dispose() { - if (isDisposed) return; - + if (isDisposed) + return; + updateCts?.Cancel(); updateCts?.Dispose(); updateCts = null; - + buffer.Complete(); - + if (updateTask is not null) { + Logger.Debug("Shutting down console update task"); + try { - updateTask.WaitWithoutException( - new CancellationTokenSource(1000).Token); + updateTask.WaitWithoutException(new CancellationTokenSource(1000).Token); updateTask.Dispose(); updateTask = null; } @@ -484,27 +518,32 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis } isDisposed = true; - + GC.SuppressFinalize(this); } - + public async ValueTask DisposeAsync() { - if (isDisposed) return; - + if (isDisposed) + return; + updateCts?.Cancel(); updateCts?.Dispose(); updateCts = null; - + if (updateTask is not null) { + Logger.Debug("Waiting for console update task shutdown..."); + await updateTask; updateTask.Dispose(); updateTask = null; + + Logger.Debug("Console update task shutdown complete"); } isDisposed = true; - + GC.SuppressFinalize(this); } } From 523e57534548364b9ad15ec1351bf07ed291fa68 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 7 Sep 2023 18:16:26 -0700 Subject: [PATCH 16/29] Fix text display & change line offset for scrollToBottomRequested in PackageModificationDialog --- .../ViewModels/Dialogs/InstallerViewModel.cs | 12 ++++++++---- .../Views/Dialogs/InstallerDialog.axaml | 2 +- .../Views/Dialogs/PackageModificationDialog.axaml | 4 ++-- .../Views/Dialogs/PackageModificationDialog.axaml.cs | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs index 68c9b96b..cda3e11a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs @@ -69,9 +69,11 @@ public partial class InstallerViewModel : ContentDialogViewModelBase private bool isAdvancedMode; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanInstall))] private bool showDuplicateWarning; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanInstall))] private string? installName; [ObservableProperty] @@ -108,6 +110,8 @@ public partial class InstallerViewModel : ContentDialogViewModelBase AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease); public bool ShowTorchVersionOptions => SelectedTorchVersion != TorchVersion.None; + public bool CanInstall => !string.IsNullOrWhiteSpace(InstallName) && !ShowDuplicateWarning; + public ProgressViewModel InstallProgress { get; } = new(); public IEnumerable Steps { get; set; } @@ -303,7 +307,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main"); // If still not found, just use the first one - version ??= AvailableVersions[0]; + version ??= AvailableVersions.FirstOrDefault(); SelectedVersion = version; } @@ -388,14 +392,14 @@ public partial class InstallerViewModel : ContentDialogViewModelBase return; AvailableCommits = new ObservableCollection(commits); - SelectedCommit = AvailableCommits[0]; + SelectedCommit = AvailableCommits.FirstOrDefault(); UpdateSelectedVersionToLatestMain(); } InstallName = SelectedPackage.DisplayName; LatestVersionText = IsReleaseMode - ? $"Latest version: {SelectedVersion.TagName}" - : $"Branch: {SelectedVersion.TagName}"; + ? $"Latest version: {SelectedVersion?.TagName}" + : $"Branch: {SelectedVersion?.TagName}"; }) .SafeFireAndForget(); } diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml index 261111ac..995eb4f2 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml @@ -197,7 +197,7 @@ Content="Install" Command="{Binding InstallCommand}" FontSize="20" - IsEnabled="{Binding !ShowDuplicateWarning}" + IsEnabled="{Binding CanInstall}" HorizontalAlignment="Center" Classes="success" Margin="4,0,8,0" diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml index aa6c4018..308ae39e 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml @@ -13,8 +13,9 @@ + HorizontalAlignment="Stretch"/> diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs index 16a59a56..2dd5053f 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs @@ -43,7 +43,7 @@ public partial class PackageModificationDialog : UserControlBase var editor = this.FindControl("Console"); if (editor?.Document == null) return; - var line = Math.Max(editor.Document.LineCount - 5, 1); + var line = Math.Max(editor.Document.LineCount - 1, 1); editor.ScrollToLine(line); }); }; From b5cb6d654af55c37d9f4e7e6b6ed7cf8b0126e90 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 7 Sep 2023 21:19:34 -0700 Subject: [PATCH 17/29] Fixed weird shared progress event --- .../Dialogs/OneClickInstallViewModel.cs | 9 +++-- .../ViewModels/LaunchPageViewModel.cs | 6 ++-- .../PackageInstallProgressItemViewModel.cs | 5 +-- .../Views/ProgressManagerPage.axaml | 6 ++-- .../PackageModification/InstallPackageStep.cs | 10 ++++-- .../Models/Packages/A3WebUI.cs | 26 ++++++++------- .../Models/Packages/BaseGitPackage.cs | 7 ++-- .../Models/Packages/BasePackage.cs | 33 +++++++++++-------- .../Models/Packages/ComfyUI.cs | 26 ++++++++------- .../Models/Packages/DankDiffusion.cs | 8 ++++- .../Models/Packages/Fooocus.cs | 12 ++++--- .../Models/Packages/InvokeAI.cs | 27 ++++++++------- .../Models/Packages/UnknownPackage.cs | 14 ++++++-- .../Models/Packages/VladAutomatic.cs | 21 +++++++----- .../Models/Packages/VoltaML.cs | 10 +++--- 15 files changed, 135 insertions(+), 85 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs index 72782cc8..3ba570c4 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs @@ -126,7 +126,6 @@ public partial class OneClickInstallViewModel : ViewModelBase // get latest version & download & install SubHeaderText = "Getting latest version..."; var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name); - SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text; var downloadVersion = new DownloadPackageVersionOptions(); var installedVersion = new InstalledPackageVersion(); @@ -204,7 +203,6 @@ public partial class OneClickInstallViewModel : ViewModelBase private async Task InstallPackage(string installLocation, TorchVersion torchVersion) { - SelectedPackage.ConsoleOutput += (_, output) => SubSubHeaderText = output.Text; SubHeaderText = "Downloading and installing package requirements..."; var progress = new Progress(progress => @@ -215,6 +213,11 @@ public partial class OneClickInstallViewModel : ViewModelBase EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress); }); - await SelectedPackage.InstallPackage(installLocation, torchVersion, progress); + await SelectedPackage.InstallPackage( + installLocation, + torchVersion, + progress, + (output) => SubSubHeaderText = output.Text + ); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs index 3d3f99f1..0d5bef52 100644 --- a/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs @@ -255,7 +255,6 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn // Unpack sitecustomize.py to venv await UnpackSiteCustomize(packagePath.JoinDir("venv")); - basePackage.ConsoleOutput += OnProcessOutputReceived; basePackage.Exited += OnProcessExited; basePackage.StartupComplete += RunningPackageOnStartupComplete; @@ -280,7 +279,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn // Use input command if provided, otherwise use package launch command command ??= basePackage.LaunchCommand; - await basePackage.RunPackage(packagePath, command, userArgsString); + await basePackage.RunPackage(packagePath, command, userArgsString, OnProcessOutputReceived); RunningPackage = basePackage; EventManager.Instance.OnRunningPackageStatusChanged( @@ -465,7 +464,6 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn // Detach handlers if (sender is BasePackage basePackage) { - basePackage.ConsoleOutput -= OnProcessOutputReceived; basePackage.Exited -= OnProcessExited; basePackage.StartupComplete -= RunningPackageOnStartupComplete; } @@ -485,7 +483,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn } // Callback for processes - private void OnProcessOutputReceived(object? sender, ProcessOutput output) + private void OnProcessOutputReceived(ProcessOutput output) { Console.Post(output); EventManager.Instance.OnScrollToBottomRequested(); diff --git a/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs index c4b5f845..70dadda1 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs @@ -12,7 +12,7 @@ using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Avalonia.ViewModels.Progress; -public partial class PackageInstallProgressItemViewModel : ProgressItemViewModelBase +public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase { private readonly IPackageModificationRunner packageModificationRunner; private BetterContentDialog? dialog; @@ -38,8 +38,9 @@ public partial class PackageInstallProgressItemViewModel : ProgressItemViewModel private void PackageModificationRunnerOnProgressChanged(object? sender, ProgressReport e) { Progress.Value = e.Percentage; - Progress.Text = e.Message; + Progress.Description = e.Message; Progress.IsIndeterminate = e.IsIndeterminate; + Progress.Text = packageModificationRunner.CurrentStep?.ProgressTitle; Name = packageModificationRunner.CurrentStep?.ProgressTitle; if (string.IsNullOrWhiteSpace(e.Message) || e.Message.Contains("Downloading...")) diff --git a/StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml b/StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml index 81740823..53525002 100644 --- a/StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml +++ b/StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml @@ -178,14 +178,14 @@ + Text="{Binding Progress.Text, Mode=OneWay}" /> - + @@ -194,7 +194,7 @@ Margin="0,4" MaxWidth="300" IsVisible="{Binding Progress.IsIndeterminate}" - Text="{Binding Progress.Text, Mode=OneWay}" /> + Text="{Binding Progress.Description, Mode=OneWay}" /> diff --git a/StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs b/StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs index 58562368..b307e161 100644 --- a/StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs +++ b/StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs @@ -1,5 +1,6 @@ using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Models.PackageModification; @@ -18,11 +19,14 @@ public class InstallPackageStep : IPackageStep public async Task ExecuteAsync(IProgress? progress = null) { - package.ConsoleOutput += (sender, output) => + void OnConsoleOutput(ProcessOutput output) { progress?.Report(new ProgressReport { IsIndeterminate = true, Message = output.Text }); - }; - await package.InstallPackage(installPath, torchVersion, progress).ConfigureAwait(false); + } + + await package + .InstallPackage(installPath, torchVersion, progress, OnConsoleOutput) + .ConfigureAwait(false); } public string ProgressTitle => "Installing package..."; diff --git a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs index 7d1505a3..91b8e90b 100644 --- a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs +++ b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs @@ -144,7 +144,8 @@ public class A3WebUI : BaseGitPackage public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false); @@ -158,16 +159,17 @@ public class A3WebUI : BaseGitPackage switch (torchVersion) { case TorchVersion.Cpu: - await InstallCpuTorch(venvRunner, progress).ConfigureAwait(false); + await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); break; case TorchVersion.Cuda: - await InstallCudaTorch(venvRunner, progress).ConfigureAwait(false); + await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); break; case TorchVersion.Rocm: - await InstallRocmTorch(venvRunner, progress).ConfigureAwait(false); + await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); break; case TorchVersion.DirectMl: - await InstallDirectMlTorch(venvRunner, progress).ConfigureAwait(false); + await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput) + .ConfigureAwait(false); break; default: throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null); @@ -179,7 +181,7 @@ public class A3WebUI : BaseGitPackage ); Logger.Info("Installing requirements_versions.txt"); await venvRunner - .PipInstall($"-r requirements_versions.txt", OnConsoleOutput) + .PipInstall($"-r requirements_versions.txt", onConsoleOutput) .ConfigureAwait(false); progress?.Report( @@ -203,14 +205,15 @@ public class A3WebUI : BaseGitPackage public override async Task RunPackage( string installedPackagePath, string command, - string arguments + string arguments, + Action? onConsoleOutput ) { await SetupVenv(installedPackagePath).ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { - OnConsoleOutput(s); + onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase)) return; @@ -231,17 +234,18 @@ public class A3WebUI : BaseGitPackage private async Task InstallRocmTorch( PyVenvRunner venvRunner, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { progress?.Report( new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true) ); - await venvRunner.PipInstall("--upgrade pip wheel", OnConsoleOutput).ConfigureAwait(false); + await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); await venvRunner - .PipInstall(PyVenvRunner.TorchPipInstallArgsRocm511, OnConsoleOutput) + .PipInstall(PyVenvRunner.TorchPipInstallArgsRocm511, onConsoleOutput) .ConfigureAwait(false); } } diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index 83e9cd15..b6d50e18 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -7,6 +7,7 @@ using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; @@ -182,7 +183,8 @@ public abstract class BaseGitPackage : BasePackage public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { await UnzipPackage(installLocation, progress).ConfigureAwait(false); @@ -269,7 +271,8 @@ public abstract class BaseGitPackage : BasePackage InstalledPackage installedPackage, TorchVersion torchVersion, IProgress? progress = null, - bool includePrerelease = false + bool includePrerelease = false, + Action? onConsoleOutput = null ) { if (installedPackage.Version == null) diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs index e3706a46..264ed427 100644 --- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs @@ -45,10 +45,16 @@ public abstract class BasePackage public abstract Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ); - public abstract Task RunPackage(string installedPackagePath, string command, string arguments); + public abstract Task RunPackage( + string installedPackagePath, + string command, + string arguments, + Action? onConsoleOutput + ); public abstract Task CheckForUpdates(InstalledPackage package); @@ -56,7 +62,8 @@ public abstract class BasePackage InstalledPackage installedPackage, TorchVersion torchVersion, IProgress? progress = null, - bool includePrerelease = false + bool includePrerelease = false, + Action? onConsoleOutput = null ); public virtual IEnumerable AvailableSharedFolderMethods => @@ -144,12 +151,9 @@ public abstract class BasePackage ); public abstract Task> GetAllBranches(); public abstract Task> GetAllReleases(); - public event EventHandler? ConsoleOutput; public event EventHandler? Exited; public event EventHandler? StartupComplete; - public void OnConsoleOutput(ProcessOutput output) => ConsoleOutput?.Invoke(this, output); - public void OnExit(int exitCode) => Exited?.Invoke(this, exitCode); public void OnStartupComplete(string url) => StartupComplete?.Invoke(this, url); @@ -161,7 +165,8 @@ public abstract class BasePackage protected async Task InstallCudaTorch( PyVenvRunner venvRunner, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { progress?.Report( @@ -169,14 +174,15 @@ public abstract class BasePackage ); await venvRunner - .PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput) + .PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, onConsoleOutput) .ConfigureAwait(false); - await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false); + await venvRunner.PipInstall("xformers", onConsoleOutput).ConfigureAwait(false); } protected async Task InstallDirectMlTorch( PyVenvRunner venvRunner, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { progress?.Report( @@ -184,13 +190,14 @@ public abstract class BasePackage ); await venvRunner - .PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, OnConsoleOutput) + .PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, onConsoleOutput) .ConfigureAwait(false); } protected async Task InstallCpuTorch( PyVenvRunner venvRunner, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { progress?.Report( @@ -198,7 +205,7 @@ public abstract class BasePackage ); await venvRunner - .PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput) + .PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, onConsoleOutput) .ConfigureAwait(false); } } diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 486ed094..90e2dc91 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -106,7 +106,8 @@ public class ComfyUI : BaseGitPackage public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false); @@ -121,16 +122,17 @@ public class ComfyUI : BaseGitPackage switch (torchVersion) { case TorchVersion.Cpu: - await InstallCpuTorch(venvRunner, progress).ConfigureAwait(false); + await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); break; case TorchVersion.Cuda: - await InstallCudaTorch(venvRunner, progress).ConfigureAwait(false); + await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); break; case TorchVersion.Rocm: - await InstallRocmTorch(venvRunner, progress).ConfigureAwait(false); + await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); break; case TorchVersion.DirectMl: - await InstallDirectMlTorch(venvRunner, progress).ConfigureAwait(false); + await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput) + .ConfigureAwait(false); break; default: throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null); @@ -141,7 +143,7 @@ public class ComfyUI : BaseGitPackage new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true) ); Logger.Info("Installing requirements.txt"); - await venvRunner.PipInstall($"-r requirements.txt", OnConsoleOutput).ConfigureAwait(false); + await venvRunner.PipInstall($"-r requirements.txt", onConsoleOutput).ConfigureAwait(false); progress?.Report( new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false) @@ -175,14 +177,15 @@ public class ComfyUI : BaseGitPackage public override async Task RunPackage( string installedPackagePath, string command, - string arguments + string arguments, + Action? onConsoleOutput ) { await SetupVenv(installedPackagePath).ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { - OnConsoleOutput(s); + onConsoleOutput?.Invoke(s); if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase)) { @@ -290,17 +293,18 @@ public class ComfyUI : BaseGitPackage private async Task InstallRocmTorch( PyVenvRunner venvRunner, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { progress?.Report( new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true) ); - await venvRunner.PipInstall("--upgrade pip wheel", OnConsoleOutput).ConfigureAwait(false); + await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); await venvRunner - .PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, OnConsoleOutput) + .PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, onConsoleOutput) .ConfigureAwait(false); } diff --git a/StabilityMatrix.Core/Models/Packages/DankDiffusion.cs b/StabilityMatrix.Core/Models/Packages/DankDiffusion.cs index 00b206a0..ea1e34b5 100644 --- a/StabilityMatrix.Core/Models/Packages/DankDiffusion.cs +++ b/StabilityMatrix.Core/Models/Packages/DankDiffusion.cs @@ -1,6 +1,7 @@ using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; @@ -29,7 +30,12 @@ public class DankDiffusion : BaseGitPackage public override Uri PreviewImageUri { get; } - public override Task RunPackage(string installedPackagePath, string command, string arguments) + public override Task RunPackage( + string installedPackagePath, + string command, + string arguments, + Action? onConsoleOutput + ) { throw new NotImplementedException(); } diff --git a/StabilityMatrix.Core/Models/Packages/Fooocus.cs b/StabilityMatrix.Core/Models/Packages/Fooocus.cs index 9ff0c7d2..f5026ff1 100644 --- a/StabilityMatrix.Core/Models/Packages/Fooocus.cs +++ b/StabilityMatrix.Core/Models/Packages/Fooocus.cs @@ -94,7 +94,8 @@ public class Fooocus : BaseGitPackage public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false); @@ -122,7 +123,7 @@ public class Fooocus : BaseGitPackage await venvRunner .PipInstall( $"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}", - OnConsoleOutput + onConsoleOutput ) .ConfigureAwait(false); @@ -130,21 +131,22 @@ public class Fooocus : BaseGitPackage new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true) ); await venvRunner - .PipInstall("-r requirements_versions.txt", OnConsoleOutput) + .PipInstall("-r requirements_versions.txt", onConsoleOutput) .ConfigureAwait(false); } public override async Task RunPackage( string installedPackagePath, string command, - string arguments + string arguments, + Action? onConsoleOutput ) { await SetupVenv(installedPackagePath).ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { - OnConsoleOutput(s); + onConsoleOutput?.Invoke(s); if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase)) { diff --git a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs index 5f71f3ba..c1f891a3 100644 --- a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs +++ b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs @@ -150,7 +150,8 @@ public class InvokeAI : BaseGitPackage public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { // Setup venv @@ -186,10 +187,10 @@ public class InvokeAI : BaseGitPackage break; } - await venvRunner.PipInstall(pipCommandArgs, OnConsoleOutput).ConfigureAwait(false); + await venvRunner.PipInstall(pipCommandArgs, onConsoleOutput).ConfigureAwait(false); await venvRunner - .PipInstall("rich packaging python-dotenv", OnConsoleOutput) + .PipInstall("rich packaging python-dotenv", onConsoleOutput) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Configuring InvokeAI", isIndeterminate: true)); @@ -198,7 +199,8 @@ public class InvokeAI : BaseGitPackage installLocation, "invokeai-configure", "--yes --skip-sd-weights", - false + false, + onConsoleOutput ) .ConfigureAwait(false); @@ -209,7 +211,8 @@ public class InvokeAI : BaseGitPackage InstalledPackage installedPackage, TorchVersion torchVersion, IProgress? progress = null, - bool includePrerelease = false + bool includePrerelease = false, + Action? onConsoleOutput = null ) { progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); @@ -260,7 +263,7 @@ public class InvokeAI : BaseGitPackage break; } - await venvRunner.PipInstall(pipCommandArgs, OnConsoleOutput).ConfigureAwait(false); + await venvRunner.PipInstall(pipCommandArgs, onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false)); @@ -276,8 +279,9 @@ public class InvokeAI : BaseGitPackage public override Task RunPackage( string installedPackagePath, string command, - string arguments - ) => RunInvokeCommand(installedPackagePath, command, arguments, true); + string arguments, + Action? onConsoleOutput + ) => RunInvokeCommand(installedPackagePath, command, arguments, true, onConsoleOutput); private async Task GetUpdateVersion( InstalledPackage installedPackage, @@ -304,7 +308,8 @@ public class InvokeAI : BaseGitPackage string installedPackagePath, string command, string arguments, - bool runDetached + bool runDetached, + Action? onConsoleOutput ) { await SetupVenv(installedPackagePath).ConfigureAwait(false); @@ -344,7 +349,7 @@ public class InvokeAI : BaseGitPackage { void HandleConsoleOutput(ProcessOutput s) { - OnConsoleOutput(s); + onConsoleOutput?.Invoke(s); if (!s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase)) return; @@ -369,7 +374,7 @@ public class InvokeAI : BaseGitPackage var result = await VenvRunner .Run($"-c \"{code}\" {arguments}".TrimEnd()) .ConfigureAwait(false); - OnConsoleOutput(new ProcessOutput { Text = result.StandardOutput }); + onConsoleOutput?.Invoke(new ProcessOutput { Text = result.StandardOutput }); } } diff --git a/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs b/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs index cd22eec6..5be7baf0 100644 --- a/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs @@ -2,6 +2,7 @@ using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Models.Packages; @@ -38,13 +39,19 @@ public class UnknownPackage : BasePackage public override Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { throw new NotImplementedException(); } - public override Task RunPackage(string installedPackagePath, string command, string arguments) + public override Task RunPackage( + string installedPackagePath, + string command, + string arguments, + Action? onConsoleOutput + ) { throw new NotImplementedException(); } @@ -102,7 +109,8 @@ public class UnknownPackage : BasePackage InstalledPackage installedPackage, TorchVersion torchVersion, IProgress? progress = null, - bool includePrerelease = false + bool includePrerelease = false, + Action? onConsoleOutput = null ) { throw new NotImplementedException(); diff --git a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs index e98b367c..b34730e7 100644 --- a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs +++ b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs @@ -161,7 +161,8 @@ public class VladAutomatic : BaseGitPackage public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { progress?.Report(new ProgressReport(-1f, "Installing package...", isIndeterminate: true)); @@ -177,23 +178,23 @@ public class VladAutomatic : BaseGitPackage // Run initial install case TorchVersion.Cuda: await venvRunner - .CustomInstall("launch.py --use-cuda --debug --test", OnConsoleOutput) + .CustomInstall("launch.py --use-cuda --debug --test", onConsoleOutput) .ConfigureAwait(false); break; case TorchVersion.Rocm: await venvRunner - .CustomInstall("launch.py --use-rocm --debug --test", OnConsoleOutput) + .CustomInstall("launch.py --use-rocm --debug --test", onConsoleOutput) .ConfigureAwait(false); break; case TorchVersion.DirectMl: await venvRunner - .CustomInstall("launch.py --use-directml --debug --test", OnConsoleOutput) + .CustomInstall("launch.py --use-directml --debug --test", onConsoleOutput) .ConfigureAwait(false); break; default: // CPU await venvRunner - .CustomInstall("launch.py --debug --test", OnConsoleOutput) + .CustomInstall("launch.py --debug --test", onConsoleOutput) .ConfigureAwait(false); break; } @@ -252,14 +253,15 @@ public class VladAutomatic : BaseGitPackage public override async Task RunPackage( string installedPackagePath, string command, - string arguments + string arguments, + Action? onConsoleOutput ) { await SetupVenv(installedPackagePath).ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { - OnConsoleOutput(s); + onConsoleOutput?.Invoke(s); if (s.Text.Contains("Running on local URL", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); @@ -287,7 +289,8 @@ public class VladAutomatic : BaseGitPackage InstalledPackage installedPackage, TorchVersion torchVersion, IProgress? progress = null, - bool includePrerelease = false + bool includePrerelease = false, + Action? onConsoleOutput = null ) { if (installedPackage.Version is null) @@ -313,7 +316,7 @@ public class VladAutomatic : BaseGitPackage venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables; await venvRunner - .CustomInstall("launch.py --upgrade --test", OnConsoleOutput) + .CustomInstall("launch.py --upgrade --test", onConsoleOutput) .ConfigureAwait(false); try diff --git a/StabilityMatrix.Core/Models/Packages/VoltaML.cs b/StabilityMatrix.Core/Models/Packages/VoltaML.cs index 79daccc2..524e863d 100644 --- a/StabilityMatrix.Core/Models/Packages/VoltaML.cs +++ b/StabilityMatrix.Core/Models/Packages/VoltaML.cs @@ -136,7 +136,8 @@ public class VoltaML : BaseGitPackage public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, - IProgress? progress = null + IProgress? progress = null, + Action? onConsoleOutput = null ) { await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false); @@ -153,7 +154,7 @@ public class VoltaML : BaseGitPackage new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true) ); await venvRunner - .PipInstall("rich packaging python-dotenv", OnConsoleOutput) + .PipInstall("rich packaging python-dotenv", onConsoleOutput) .ConfigureAwait(false); progress?.Report( @@ -164,7 +165,8 @@ public class VoltaML : BaseGitPackage public override async Task RunPackage( string installedPackagePath, string command, - string arguments + string arguments, + Action? onConsoleOutput ) { await SetupVenv(installedPackagePath).ConfigureAwait(false); @@ -175,7 +177,7 @@ public class VoltaML : BaseGitPackage void HandleConsoleOutput(ProcessOutput s) { - OnConsoleOutput(s); + onConsoleOutput?.Invoke(s); if (s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase)) { From a56cfb004f94bd854141063a1269bea569718fee Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Sep 2023 11:28:17 -0700 Subject: [PATCH 18/29] Fix some missing paths & launch args & added Foooocus-MRE --- StabilityMatrix.Avalonia/App.axaml.cs | 5 +- .../Models/Packages/A3WebUI.cs | 3 +- .../Models/Packages/ComfyUI.cs | 9 +- .../Models/Packages/FooocusMre.cs | 175 ++++++++++++++++++ 4 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 StabilityMatrix.Core/Models/Packages/FooocusMre.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 5e37768d..0865dda5 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -342,11 +342,12 @@ public sealed class App : Application internal static void ConfigurePackages(IServiceCollection services) { services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); } private static IServiceCollection ConfigureServices() diff --git a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs index 91b8e90b..2d184903 100644 --- a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs +++ b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs @@ -54,7 +54,8 @@ public class A3WebUI : BaseGitPackage [SharedFolderType.Karlo] = new[] { "models/karlo" }, [SharedFolderType.TextualInversion] = new[] { "embeddings" }, [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, - [SharedFolderType.ControlNet] = new[] { "models/ControlNet" } + [SharedFolderType.ControlNet] = new[] { "models/ControlNet" }, + [SharedFolderType.Codeformer] = new[] { "models/Codeformer" }, }; [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 90e2dc91..1c30940b 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -76,10 +76,17 @@ public class ComfyUI : BaseGitPackage Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } }, new() + { + Name = "Enable DirectML", + Type = LaunchOptionType.Bool, + InitialValue = HardwareHelper.PreferDirectML(), + Options = { "--directml" } + }, + new() { Name = "Use CPU only", Type = LaunchOptionType.Bool, - InitialValue = !HardwareHelper.HasNvidiaGpu(), + InitialValue = !HardwareHelper.HasNvidiaGpu() && !HardwareHelper.HasAmdGpu(), Options = { "--cpu" } }, new() diff --git a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs new file mode 100644 index 00000000..5fd3e043 --- /dev/null +++ b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs @@ -0,0 +1,175 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Processes; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Core.Models.Packages; + +public class FooocusMre : BaseGitPackage +{ + public FooocusMre( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper + ) + : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } + + public override string Name => "Fooocus-MRE"; + public override string DisplayName { get; set; } = "Fooocus-MRE"; + public override string Author => "MoonRide303"; + + public override string Blurb => + "Fooocus-MRE is an image generating software, enhanced variant of the original Fooocus dedicated for a bit more advanced users"; + + public override string LicenseType => "GPL-3.0"; + + public override string LicenseUrl => + "https://github.com/MoonRide303/Fooocus-MRE/blob/moonride-main/LICENSE"; + public override string LaunchCommand => "launch.py"; + + public override Uri PreviewImageUri => + new( + "https://user-images.githubusercontent.com/130458190/265366059-ce430ea0-0995-4067-98dd-cef1d7dc1ab6.png" + ); + + public override List LaunchOptions => + new() + { + new LaunchOptionDefinition + { + Name = "Port", + Type = LaunchOptionType.String, + Description = "Sets the listen port", + Options = { "--port" } + }, + new LaunchOptionDefinition + { + Name = "Share", + Type = LaunchOptionType.Bool, + Description = "Set whether to share on Gradio", + Options = { "--share" } + }, + new LaunchOptionDefinition + { + Name = "Listen", + Type = LaunchOptionType.String, + Description = "Set the listen interface", + Options = { "--listen" } + }, + LaunchOptionDefinition.Extras + }; + + public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; + + public override IEnumerable AvailableSharedFolderMethods => + new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None }; + + public override Dictionary> SharedFolders => + new() + { + [SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" }, + [SharedFolderType.Diffusers] = new[] { "models/diffusers" }, + [SharedFolderType.Lora] = new[] { "models/loras" }, + [SharedFolderType.CLIP] = new[] { "models/clip" }, + [SharedFolderType.TextualInversion] = new[] { "models/embeddings" }, + [SharedFolderType.VAE] = new[] { "models/vae" }, + [SharedFolderType.ApproxVAE] = new[] { "models/vae_approx" }, + [SharedFolderType.ControlNet] = new[] { "models/controlnet" }, + [SharedFolderType.GLIGEN] = new[] { "models/gligen" }, + [SharedFolderType.ESRGAN] = new[] { "models/upscale_models" }, + [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" } + }; + + public override IEnumerable AvailableTorchVersions => + new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm }; + + public override async Task GetLatestVersion() + { + var release = await GetLatestRelease().ConfigureAwait(false); + return release.TagName!; + } + + public override async Task InstallPackage( + string installLocation, + TorchVersion torchVersion, + IProgress? progress = null, + Action? onConsoleOutput = null + ) + { + await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false); + var venvRunner = await SetupVenv(installLocation, forceRecreate: true) + .ConfigureAwait(false); + + progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); + + var torchVersionStr = "cpu"; + + switch (torchVersion) + { + case TorchVersion.Cuda: + torchVersionStr = "cu118"; + break; + case TorchVersion.Rocm: + torchVersionStr = "rocm5.4.2"; + break; + case TorchVersion.Cpu: + break; + default: + throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null); + } + + await venvRunner + .PipInstall( + $"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}", + onConsoleOutput + ) + .ConfigureAwait(false); + + progress?.Report( + new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true) + ); + await venvRunner + .PipInstall("-r requirements_versions.txt", onConsoleOutput) + .ConfigureAwait(false); + } + + public override async Task RunPackage( + string installedPackagePath, + string command, + string arguments, + Action? onConsoleOutput + ) + { + await SetupVenv(installedPackagePath).ConfigureAwait(false); + + void HandleConsoleOutput(ProcessOutput s) + { + onConsoleOutput?.Invoke(s); + + if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase)) + { + var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); + var match = regex.Match(s.Text); + if (match.Success) + { + WebUrl = match.Value; + } + OnStartupComplete(WebUrl); + } + } + + void HandleExit(int i) + { + Debug.WriteLine($"Venv process exited with code {i}"); + OnExit(i); + } + + var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}"; + + VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit); + } +} From 106d2c81b9479cad55be80de346243777abbf35d Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Sep 2023 11:31:24 -0700 Subject: [PATCH 19/29] chagenlog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f764e7ba..ae22f97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,17 @@ 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.4.0 +### Added +- New installable Package - [Fooocus-MRE](https://github.com/MoonRide303/Fooocus-MRE) ### Changed - Revamped package installer - Added "advanced options" section for commit, shared folder method, and pytorch options - Can be run in the background - Shows progress in the Downloads tab +### Fixed +- Fixed [#97](https://github.com/LykosAI/StabilityMatrix/issues/97) - Codeformer folder should now get linked correctly +- Fixed [#106](https://github.com/LykosAI/StabilityMatrix/issues/106) - ComfyUI should now install correctly on Windows machines with an AMD GPU using DirectML +- Fixed [#107](https://github.com/LykosAI/StabilityMatrix/issues/107) - Added `--autolaunch` option to SD.Next ## v2.3.4 ### Fixed From 578e4d72050c9fc8fb33d6d8765036ae8d641de2 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Sep 2023 11:35:39 -0700 Subject: [PATCH 20/29] Add autolaunch option for A1111 also --- StabilityMatrix.Core/Models/Packages/A3WebUI.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs index 2d184903..414dab46 100644 --- a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs +++ b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs @@ -106,6 +106,13 @@ public class A3WebUI : BaseGitPackage Options = new() { "--api" } }, new() + { + Name = "Auto Launch Web UI", + Type = LaunchOptionType.Bool, + InitialValue = false, + Options = new() { "--autolaunch" } + }, + new() { Name = "Skip Torch CUDA Check", Type = LaunchOptionType.Bool, From 4e9348c7bbcfea8de0a5e05eb93176b33bf6f963 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Sep 2023 11:48:33 -0700 Subject: [PATCH 21/29] Fix host -> server-name arg --- StabilityMatrix.Core/Models/Packages/A3WebUI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs index 414dab46..443cf802 100644 --- a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs +++ b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs @@ -67,7 +67,7 @@ public class A3WebUI : BaseGitPackage Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", - Options = new() { "--host" } + Options = new() { "--server-name" } }, new() { From 370ed5a02612294ab2017ea3f38ccf0443d0f6ff Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Sep 2023 14:02:14 -0700 Subject: [PATCH 22/29] Send page number in request for Installed models --- .../ViewModels/CheckpointBrowserViewModel.cs | 372 ++++++++++++------ 1 file changed, 252 insertions(+), 120 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs index 42bad566..f7cc495e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs @@ -51,48 +51,93 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase private readonly ILiteDbContext liteDbContext; private readonly INotificationService notificationService; private const int MaxModelsPerPage = 14; - private LRUCache cache = new(50); - - [ObservableProperty] private ObservableCollection? modelCards; - [ObservableProperty] private DataGridCollectionView? modelCardsView; - - [ObservableProperty] private string searchQuery = string.Empty; - [ObservableProperty] private bool showNsfw; - [ObservableProperty] private bool showMainLoadingSpinner; - [ObservableProperty] private CivitPeriod selectedPeriod = CivitPeriod.Month; - [ObservableProperty] private CivitSortMode sortMode = CivitSortMode.HighestRated; - [ObservableProperty] private CivitModelType selectedModelType = CivitModelType.Checkpoint; - [ObservableProperty] private int currentPageNumber; - [ObservableProperty] private int totalPages; - [ObservableProperty] private bool hasSearched; - [ObservableProperty] private bool canGoToNextPage; - [ObservableProperty] private bool canGoToPreviousPage; - [ObservableProperty] private bool canGoToFirstPage; - [ObservableProperty] private bool canGoToLastPage; - [ObservableProperty] private bool isIndeterminate; - [ObservableProperty] private bool noResultsFound; - [ObservableProperty] private string noResultsText = string.Empty; - [ObservableProperty] private string selectedBaseModelType = "All"; - + private LRUCache< + int /* model id */ + , + CheckpointBrowserCardViewModel + > cache = new(50); + + [ObservableProperty] + private ObservableCollection? modelCards; + + [ObservableProperty] + private DataGridCollectionView? modelCardsView; + + [ObservableProperty] + private string searchQuery = string.Empty; + + [ObservableProperty] + private bool showNsfw; + + [ObservableProperty] + private bool showMainLoadingSpinner; + + [ObservableProperty] + private CivitPeriod selectedPeriod = CivitPeriod.Month; + + [ObservableProperty] + private CivitSortMode sortMode = CivitSortMode.HighestRated; + + [ObservableProperty] + private CivitModelType selectedModelType = CivitModelType.Checkpoint; + + [ObservableProperty] + private int currentPageNumber; + + [ObservableProperty] + private int totalPages; + + [ObservableProperty] + private bool hasSearched; + + [ObservableProperty] + private bool canGoToNextPage; + + [ObservableProperty] + private bool canGoToPreviousPage; + + [ObservableProperty] + private bool canGoToFirstPage; + + [ObservableProperty] + private bool canGoToLastPage; + + [ObservableProperty] + private bool isIndeterminate; + + [ObservableProperty] + private bool noResultsFound; + + [ObservableProperty] + private string noResultsText = string.Empty; + + [ObservableProperty] + private string selectedBaseModelType = "All"; + private List allModelCards = new(); - - public IEnumerable AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast(); - public IEnumerable AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast(); - public IEnumerable AllModelTypes => Enum.GetValues(typeof(CivitModelType)) - .Cast() - .Where(t => t == CivitModelType.All || t.ConvertTo() > 0) - .OrderBy(t => t.ToString()); + public IEnumerable AllCivitPeriods => + Enum.GetValues(typeof(CivitPeriod)).Cast(); + public IEnumerable AllSortModes => + Enum.GetValues(typeof(CivitSortMode)).Cast(); + + public IEnumerable AllModelTypes => + Enum.GetValues(typeof(CivitModelType)) + .Cast() + .Where(t => t == CivitModelType.All || t.ConvertTo() > 0) + .OrderBy(t => t.ToString()); - public List BaseModelOptions => new() {"All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0"}; + public List BaseModelOptions => + new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" }; public CheckpointBrowserViewModel( - ICivitApi civitApi, - IDownloadService downloadService, + ICivitApi civitApi, + IDownloadService downloadService, ISettingsManager settingsManager, ServiceManager dialogFactory, ILiteDbContext liteDbContext, - INotificationService notificationService) + INotificationService notificationService + ) { this.civitApi = civitApi; this.downloadService = downloadService; @@ -117,27 +162,38 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase public override void OnLoaded() { - if (Design.IsDesignMode) return; - + if (Design.IsDesignMode) + return; + var searchOptions = settingsManager.Settings.ModelSearchOptions; - + // Fix SelectedModelType if someone had selected the obsolete "Model" option - if (searchOptions is {SelectedModelType: CivitModelType.Model}) + if (searchOptions is { SelectedModelType: CivitModelType.Model }) { - settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( - SelectedPeriod, SortMode, CivitModelType.Checkpoint, SelectedBaseModelType)); + settingsManager.Transaction( + s => + s.ModelSearchOptions = new ModelSearchOptions( + SelectedPeriod, + SortMode, + CivitModelType.Checkpoint, + SelectedBaseModelType + ) + ); searchOptions = settingsManager.Settings.ModelSearchOptions; } - + SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month; SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated; SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint; SelectedBaseModelType = searchOptions?.SelectedBaseModelType ?? "All"; - + ShowNsfw = settingsManager.Settings.ModelBrowserNsfwEnabled; - - settingsManager.RelayPropertyFor(this, model => model.ShowNsfw, - settings => settings.ModelBrowserNsfwEnabled); + + settingsManager.RelayPropertyFor( + this, + model => model.ShowNsfw, + settings => settings.ModelBrowserNsfwEnabled + ); } /// @@ -145,7 +201,8 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase /// private bool FilterModelCardsPredicate(object? item) { - if (item is not CheckpointBrowserCardViewModel card) return false; + if (item is not CheckpointBrowserCardViewModel card) + return false; return !card.CivitModel.Nsfw || ShowNsfw; } @@ -162,38 +219,52 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase var models = modelsResponse.Items; if (models is null) { - Logger.Debug("CivitAI Query {Text} returned no results (in {Elapsed:F1} s)", - queryText, timer.Elapsed.TotalSeconds); + Logger.Debug( + "CivitAI Query {Text} returned no results (in {Elapsed:F1} s)", + queryText, + timer.Elapsed.TotalSeconds + ); return; } - Logger.Debug("CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)", - queryText, models.Count, timer.Elapsed.TotalSeconds); + Logger.Debug( + "CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)", + queryText, + models.Count, + timer.Elapsed.TotalSeconds + ); var unknown = models.Where(m => m.Type == CivitModelType.Unknown).ToList(); if (unknown.Any()) { var names = unknown.Select(m => m.Name).ToList(); - Logger.Warn("Excluded {Unknown} unknown model types: {Models}", unknown.Count, - names); + Logger.Warn( + "Excluded {Unknown} unknown model types: {Models}", + unknown.Count, + names + ); } // Filter out unknown model types and archived/taken-down models - models = models.Where(m => m.Type.ConvertTo() > 0) - .Where(m => m.Mode == null).ToList(); + models = models + .Where(m => m.Type.ConvertTo() > 0) + .Where(m => m.Mode == null) + .ToList(); // Database update calls will invoke `OnModelsUpdated` // Add to database await liteDbContext.UpsertCivitModelAsync(models); // Add as cache entry - var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync(new() - { - Id = ObjectHash.GetMd5Guid(request), - InsertedAt = DateTimeOffset.UtcNow, - Request = request, - Items = models, - Metadata = modelsResponse.Metadata - }); + var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync( + new() + { + Id = ObjectHash.GetMd5Guid(request), + InsertedAt = DateTimeOffset.UtcNow, + Request = request, + Items = models, + Metadata = modelsResponse.Metadata + } + ); if (cacheNew) { @@ -207,26 +278,42 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase } catch (OperationCanceledException) { - notificationService.Show(new Notification("Request to CivitAI timed out", - "Please try again in a few minutes")); + notificationService.Show( + new Notification( + "Request to CivitAI timed out", + "Please try again in a few minutes" + ) + ); Logger.Warn($"CivitAI query timed out ({request})"); } catch (HttpRequestException e) { - notificationService.Show(new Notification("CivitAI can't be reached right now", - "Please try again in a few minutes")); + notificationService.Show( + new Notification( + "CivitAI can't be reached right now", + "Please try again in a few minutes" + ) + ); Logger.Warn(e, $"CivitAI query HttpRequestException ({request})"); } catch (ApiException e) { - notificationService.Show(new Notification("CivitAI can't be reached right now", - "Please try again in a few minutes")); + notificationService.Show( + new Notification( + "CivitAI can't be reached right now", + "Please try again in a few minutes" + ) + ); Logger.Warn(e, $"CivitAI query ApiException ({request})"); } catch (Exception e) { - notificationService.Show(new Notification("CivitAI can't be reached right now", - $"Unknown exception during CivitAI query: {e.GetType().Name}")); + notificationService.Show( + new Notification( + "CivitAI can't be reached right now", + $"Unknown exception during CivitAI query: {e.GetType().Name}" + ) + ); Logger.Error(e, $"CivitAI query unknown exception ({request})"); } finally @@ -235,11 +322,11 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase UpdateResultsText(); } } - + /// /// Updates model cards using api response object. /// - private void UpdateModelCards(IEnumerable? models, CivitMetadata? metadata) + private void UpdateModelCards(IEnumerable? models, CivitMetadata? metadata) { if (models is null) { @@ -266,26 +353,29 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase vm.CivitModel = model; vm.OnDownloadStart = viewModel => { - if (cache.Get(viewModel.CivitModel.Id) != null) return; + if (cache.Get(viewModel.CivitModel.Id) != null) + return; cache.Add(viewModel.CivitModel.Id, viewModel); }; return vm; }); - + return newCard; - }).ToList(); - + }) + .ToList(); + allModelCards = updateCards; var filteredCards = updateCards.Where(FilterModelCardsPredicate); if (SortMode == CivitSortMode.Installed) { - filteredCards = - filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available"); + filteredCards = filteredCards.OrderByDescending( + x => x.UpdateCardText == "Update Available" + ); } - - ModelCards =new ObservableCollection(filteredCards); + + ModelCards = new ObservableCollection(filteredCards); } TotalPages = metadata?.TotalPages ?? 1; CanGoToFirstPage = CurrentPageNumber != 1; @@ -304,14 +394,14 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase private async Task SearchModels() { var timer = Stopwatch.StartNew(); - + if (SearchQuery != previousSearchQuery) { // Reset page number CurrentPageNumber = 1; previousSearchQuery = SearchQuery; } - + // Build request var modelRequest = new CivitModelsRequest { @@ -334,10 +424,10 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase { modelRequest.Query = SearchQuery; } - + if (SelectedModelType != CivitModelType.All) { - modelRequest.Types = new[] {SelectedModelType}; + modelRequest.Types = new[] { SelectedModelType }; } if (SelectedBaseModelType != "All") @@ -347,9 +437,9 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase if (SortMode == CivitSortMode.Installed) { - var connectedModels = - CheckpointFile.GetAllCheckpointFiles(settingsManager.ModelsDirectory) - .Where(c => c.IsConnectedModel); + var connectedModels = CheckpointFile + .GetAllCheckpointFiles(settingsManager.ModelsDirectory) + .Where(c => c.IsConnectedModel); if (SelectedModelType != CivitModelType.All) { @@ -358,24 +448,34 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase modelRequest = new CivitModelsRequest { - CommaSeparatedModelIds = string.Join(",", - connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m) - .Select(g => g.First())), - Types = SelectedModelType == CivitModelType.All ? null : new[] {SelectedModelType} + CommaSeparatedModelIds = string.Join( + ",", + connectedModels + .Select(c => c.ConnectedModel!.ModelId) + .GroupBy(m => m) + .Select(g => g.First()) + ), + Types = + SelectedModelType == CivitModelType.All ? null : new[] { SelectedModelType }, + Page = CurrentPageNumber, }; } - + // See if query is cached var cachedQuery = await liteDbContext.CivitModelQueryCache .IncludeAll() .FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest)); - + // If cached, update model cards if (cachedQuery is not null) { var elapsed = timer.Elapsed; - Logger.Debug("Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)", - SearchQuery, modelRequest.GetHashCode(), elapsed.TotalSeconds); + Logger.Debug( + "Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)", + SearchQuery, + modelRequest.GetHashCode(), + elapsed.TotalSeconds + ); UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata); // Start remote query (background mode) @@ -386,7 +486,8 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase CivitModelQuery(modelRequest).SafeFireAndForget(); Logger.Debug( "Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query", - timeSinceCache.Value.TotalSeconds); + timeSinceCache.Value.TotalSeconds + ); } } else @@ -395,7 +496,7 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase ShowMainLoadingSpinner = true; await CivitModelQuery(modelRequest); } - + UpdateResultsText(); } @@ -406,17 +507,17 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase public void PreviousPage() { - if (CurrentPageNumber == 1) + if (CurrentPageNumber == 1) return; - + CurrentPageNumber--; } - + public void NextPage() { - if (CurrentPageNumber == TotalPages) + if (CurrentPageNumber == TotalPages) return; - + CurrentPageNumber++; } @@ -429,46 +530,75 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase { settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value); // ModelCardsView?.Refresh(); - var updateCards = allModelCards - .Where(FilterModelCardsPredicate); + var updateCards = allModelCards.Where(FilterModelCardsPredicate); ModelCards = new ObservableCollection(updateCards); - if (!HasSearched) return; - + if (!HasSearched) + return; + UpdateResultsText(); } partial void OnSelectedPeriodChanged(CivitPeriod value) { TrySearchAgain().SafeFireAndForget(); - settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( - value, SortMode, SelectedModelType, SelectedBaseModelType)); + settingsManager.Transaction( + s => + s.ModelSearchOptions = new ModelSearchOptions( + value, + SortMode, + SelectedModelType, + SelectedBaseModelType + ) + ); } partial void OnSortModeChanged(CivitSortMode value) { TrySearchAgain().SafeFireAndForget(); - settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( - SelectedPeriod, value, SelectedModelType, SelectedBaseModelType)); + settingsManager.Transaction( + s => + s.ModelSearchOptions = new ModelSearchOptions( + SelectedPeriod, + value, + SelectedModelType, + SelectedBaseModelType + ) + ); } - + partial void OnSelectedModelTypeChanged(CivitModelType value) { TrySearchAgain().SafeFireAndForget(); - settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( - SelectedPeriod, SortMode, value, SelectedBaseModelType)); + settingsManager.Transaction( + s => + s.ModelSearchOptions = new ModelSearchOptions( + SelectedPeriod, + SortMode, + value, + SelectedBaseModelType + ) + ); } partial void OnSelectedBaseModelTypeChanged(string value) { TrySearchAgain().SafeFireAndForget(); - settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( - SelectedPeriod, SortMode, SelectedModelType, value)); + settingsManager.Transaction( + s => + s.ModelSearchOptions = new ModelSearchOptions( + SelectedPeriod, + SortMode, + SelectedModelType, + value + ) + ); } private async Task TrySearchAgain(bool shouldUpdatePageNumber = true) { - if (!HasSearched) return; + if (!HasSearched) + return; ModelCards?.Clear(); if (shouldUpdatePageNumber) @@ -483,11 +613,13 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase private void UpdateResultsText() { NoResultsFound = ModelCards?.Count <= 0; - NoResultsText = allModelCards.Count > 0 - ? $"{allModelCards.Count} results hidden by filters" - : "No results found"; + NoResultsText = + allModelCards.Count > 0 + ? $"{allModelCards.Count} results hidden by filters" + : "No results found"; } public override string Title => "Model Browser"; - public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true }; + public override IconSource IconSource => + new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true }; } From c1f924f8799080a5b199357ddfd2e064a617924f Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 10 Sep 2023 16:01:15 -0700 Subject: [PATCH 23/29] Moved more xaml strings to Resources for localization & fix connected model imports not adding to installedmodels list --- CHANGELOG.md | 4 +- .../Languages/Resources.Designer.cs | 531 ++++++++++++++++++ .../Languages/Resources.ja-JP.resx | 33 ++ .../Languages/Resources.resx | 177 ++++++ .../Views/CheckpointBrowserPage.axaml | 31 +- .../Views/CheckpointsPage.axaml | 34 +- .../Views/Dialogs/ExceptionDialog.axaml | 5 +- .../Views/Dialogs/InstallerDialog.axaml | 23 +- .../Views/Dialogs/OneClickInstallDialog.axaml | 5 +- .../Dialogs/PackageModificationDialog.axaml | 5 +- .../Dialogs/SelectDataDirectoryDialog.axaml | 13 +- .../Dialogs/SelectModelVersionDialog.axaml | 11 +- .../Views/Dialogs/UpdateDialog.axaml | 3 +- .../Views/FirstLaunchSetupWindow.axaml | 11 +- .../Views/MainWindow.axaml | 9 +- StabilityMatrix/Models/CheckpointFolder.cs | 169 +++--- 16 files changed, 930 insertions(+), 134 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae22f97c..179d26d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 ### Fixed - Fixed [#97](https://github.com/LykosAI/StabilityMatrix/issues/97) - Codeformer folder should now get linked correctly - Fixed [#106](https://github.com/LykosAI/StabilityMatrix/issues/106) - ComfyUI should now install correctly on Windows machines with an AMD GPU using DirectML -- Fixed [#107](https://github.com/LykosAI/StabilityMatrix/issues/107) - Added `--autolaunch` option to SD.Next +- Fixed [#107](https://github.com/LykosAI/StabilityMatrix/issues/107) - Added `--autolaunch` option to SD.Next +- Fixed [#110](https://github.com/LykosAI/StabilityMatrix/issues/110) - Model Browser should properly navigate to the next page of Installed models +- Installed tag on model browser should now show for connected models imported via drag & drop ## v2.3.4 ### Fixed diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index f107c620..f27f2232 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -68,6 +68,42 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Close. + /// + public static string Action_Close { + get { + return ResourceManager.GetString("Action_Close", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Continue. + /// + public static string Action_Continue { + get { + return ResourceManager.GetString("Action_Continue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete. + /// + public static string Action_Delete { + get { + return ResourceManager.GetString("Action_Delete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exit Application. + /// + public static string Action_ExitApplication { + get { + return ResourceManager.GetString("Action_ExitApplication", resourceCulture); + } + } + /// /// Looks up a localized string similar to Import. /// @@ -77,6 +113,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Install. + /// + public static string Action_Install { + get { + return ResourceManager.GetString("Action_Install", resourceCulture); + } + } + /// /// Looks up a localized string similar to Launch. /// @@ -86,6 +131,24 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to New. + /// + public static string Action_New { + get { + return ResourceManager.GetString("Action_New", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open on CivitAI. + /// + public static string Action_OpenOnCivitAi { + get { + return ResourceManager.GetString("Action_OpenOnCivitAi", resourceCulture); + } + } + /// /// Looks up a localized string similar to Quit. /// @@ -113,6 +176,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Rename. + /// + public static string Action_Rename { + get { + return ResourceManager.GetString("Action_Rename", resourceCulture); + } + } + /// /// Looks up a localized string similar to Save. /// @@ -122,6 +194,60 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Search. + /// + public static string Action_Search { + get { + return ResourceManager.GetString("Action_Search", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show in Explorer. + /// + public static string Action_ShowInExplorer { + get { + return ResourceManager.GetString("Action_ShowInExplorer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Advanced Options. + /// + public static string Label_AdvancedOptions { + get { + return ResourceManager.GetString("Label_AdvancedOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All Versions. + /// + public static string Label_AllVersions { + get { + return ResourceManager.GetString("Label_AllVersions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Base Model. + /// + public static string Label_BaseModel { + get { + return ResourceManager.GetString("Label_BaseModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Become a Patron. + /// + public static string Label_BecomeAPatron { + get { + return ResourceManager.GetString("Label_BecomeAPatron", resourceCulture); + } + } + /// /// Looks up a localized string similar to Branches. /// @@ -131,6 +257,87 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Categories. + /// + public static string Label_Categories { + get { + return ResourceManager.GetString("Label_Categories", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close dialog when finished. + /// + public static string Label_CloseDialogWhenFinished { + get { + return ResourceManager.GetString("Label_CloseDialogWhenFinished", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Commit. + /// + public static string Label_Commit { + get { + return ResourceManager.GetString("Label_Commit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connected Model. + /// + public static string Label_ConnectedModel { + get { + return ResourceManager.GetString("Label_ConnectedModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data Directory. + /// + public static string Label_DataDirectory { + get { + return ResourceManager.GetString("Label_DataDirectory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is where model checkpoints, LORAs, web UIs, settings, etc. will be installed.. + /// + public static string Label_DataDirectoryExplanation { + get { + return ResourceManager.GetString("Label_DataDirectoryExplanation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data provided by CivitAI. + /// + public static string Label_DataProvidedByCivitAi { + get { + return ResourceManager.GetString("Label_DataProvidedByCivitAi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display Name. + /// + public static string Label_DisplayName { + get { + return ResourceManager.GetString("Label_DisplayName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Downloads. + /// + public static string Label_Downloads { + get { + return ResourceManager.GetString("Label_Downloads", resourceCulture); + } + } + /// /// Looks up a localized string similar to Drag & Drop checkpoints here to import. /// @@ -140,6 +347,96 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Drop file here to import. + /// + public static string Label_DropFileToImport { + get { + return ResourceManager.GetString("Label_DropFileToImport", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience.. + /// + public static string Label_FatWarning { + get { + return ResourceManager.GetString("Label_FatWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First Page. + /// + public static string Label_FirstPage { + get { + return ResourceManager.GetString("Label_FirstPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Folder. + /// + public static string Label_Folder { + get { + return ResourceManager.GetString("Label_Folder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Import as Connected. + /// + public static string Label_ImportAsConnected { + get { + return ResourceManager.GetString("Label_ImportAsConnected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search for connected metadata on new local imports. + /// + public static string Label_ImportAsConnectedExplanation { + get { + return ResourceManager.GetString("Label_ImportAsConnectedExplanation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Import Latest -. + /// + public static string Label_ImportLatest { + get { + return ResourceManager.GetString("Label_ImportLatest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indexing.... + /// + public static string Label_Indexing { + get { + return ResourceManager.GetString("Label_Indexing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An installation with this name already exists.. + /// + public static string Label_InstallationWithThisNameExists { + get { + return ResourceManager.GetString("Label_InstallationWithThisNameExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Join Discord Server. + /// + public static string Label_JoinDiscord { + get { + return ResourceManager.GetString("Label_JoinDiscord", resourceCulture); + } + } + /// /// Looks up a localized string similar to Language. /// @@ -149,6 +446,105 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Last Page. + /// + public static string Label_LastPage { + get { + return ResourceManager.GetString("Label_LastPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Let's get started. + /// + public static string Label_LetsGetStarted { + get { + return ResourceManager.GetString("Label_LetsGetStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to License Agreement.. + /// + public static string Label_LicenseAgreement { + get { + return ResourceManager.GetString("Label_LicenseAgreement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Local Model. + /// + public static string Label_LocalModel { + get { + return ResourceManager.GetString("Label_LocalModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Model Description. + /// + public static string Label_ModelDescription { + get { + return ResourceManager.GetString("Label_ModelDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search models, #tags, or @users. + /// + public static string Label_ModelSearchWatermark { + get { + return ResourceManager.GetString("Label_ModelSearchWatermark", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Models Folder. + /// + public static string Label_ModelsFolder { + get { + return ResourceManager.GetString("Label_ModelsFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Model Type. + /// + public static string Label_ModelType { + get { + return ResourceManager.GetString("Label_ModelType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A new version of Stability Matrix is available!. + /// + public static string Label_NewVersionAvailable { + get { + return ResourceManager.GetString("Label_NewVersionAvailable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Next Image. + /// + public static string Label_NextImage { + get { + return ResourceManager.GetString("Label_NextImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Next Page. + /// + public static string Label_NextPage { + get { + return ResourceManager.GetString("Label_NextPage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Package Type. /// @@ -158,6 +554,78 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Page. + /// + public static string Label_Page { + get { + return ResourceManager.GetString("Label_Page", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please choose a different name or select a different install location.. + /// + public static string Label_PleaseChooseDifferentName { + get { + return ResourceManager.GetString("Label_PleaseChooseDifferentName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Portable Mode. + /// + public static string Label_PortableMode { + get { + return ResourceManager.GetString("Label_PortableMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In Portable Mode, all data and settings will be stored in the same directory as the application. You will be able to move the application with its 'Data' folder to a different location or computer.. + /// + public static string Label_PortableModeExplanation { + get { + return ResourceManager.GetString("Label_PortableModeExplanation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Previous Image. + /// + public static string Label_PreviousImage { + get { + return ResourceManager.GetString("Label_PreviousImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Previous Page. + /// + public static string Label_PreviousPage { + get { + return ResourceManager.GetString("Label_PreviousPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PyTorch Version. + /// + public static string Label_PyTorchVersion { + get { + return ResourceManager.GetString("Label_PyTorchVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I have read and agree to the. + /// + public static string Label_ReadAndAgree { + get { + return ResourceManager.GetString("Label_ReadAndAgree", resourceCulture); + } + } + /// /// Looks up a localized string similar to Relaunch Required. /// @@ -176,6 +644,60 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Shared Model Folder Strategy. + /// + public static string Label_SharedModelFolderStrategy { + get { + return ResourceManager.GetString("Label_SharedModelFolderStrategy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show NSFW Content. + /// + public static string Label_ShowNsfwContent { + get { + return ResourceManager.GetString("Label_ShowNsfwContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Skip first-time setup. + /// + public static string Label_SkipSetup { + get { + return ResourceManager.GetString("Label_SkipSetup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sort. + /// + public static string Label_Sort { + get { + return ResourceManager.GetString("Label_Sort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Period. + /// + public static string Label_TimePeriod { + get { + return ResourceManager.GetString("Label_TimePeriod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An unexpected error occurred. + /// + public static string Label_UnexpectedErrorOccurred { + get { + return ResourceManager.GetString("Label_UnexpectedErrorOccurred", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unknown Package. /// @@ -185,6 +707,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Update Available. + /// + public static string Label_UpdateAvailable { + get { + return ResourceManager.GetString("Label_UpdateAvailable", resourceCulture); + } + } + /// /// Looks up a localized string similar to Version. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx index ccd583bf..4fcc66bd 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx @@ -20,4 +20,37 @@ 言語 + + アプリケーションを終了する + + + インポート + + + インストール + + + 打ち上げる + + + 再起動する + + + やめる + + + 後で再起動する + + + パトロンになる + + + ブランチ + + + 期間 + + + 注文 + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 414b5870..a825e8bb 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -69,4 +69,181 @@ Drag & Drop checkpoints here to import + + Update Available + + + Become a Patron + + + Join Discord Server + + + Downloads + + + Install + + + Skip first-time setup + + + An unexpected error occurred + + + Exit Application + + + Display Name + + + An installation with this name already exists. + + + Please choose a different name or select a different install location. + + + Advanced Options + + + Commit + + + Shared Model Folder Strategy + + + PyTorch Version + + + Close dialog when finished + + + Close + + + Data Directory + + + This is where model checkpoints, LORAs, web UIs, settings, etc. will be installed. + + + You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience. + + + Portable Mode + + + In Portable Mode, all data and settings will be stored in the same directory as the application. You will be able to move the application with its 'Data' folder to a different location or computer. + + + Continue + + + Previous Image + + + Next Image + + + Model Description + + + A new version of Stability Matrix is available! + + + Import Latest - + + + All Versions + + + Search models, #tags, or @users + + + Search + + + Sort + + + Period + + + Model Type + + + Base Model + + + Show NSFW Content + + + Data provided by CivitAI + + + Page + + + First Page + + + Previous Page + + + Next Page + + + Last Page + + + Rename + + + Delete + + + Open on CivitAI + + + Connected Model + + + Local Model + + + Show in Explorer + + + New + + + Folder + + + Drop file here to import + + + Import as Connected + + + Search for connected metadata on new local imports + + + Indexing... + + + Models Folder + + + Categories + + + Let's get started + + + I have read and agree to the + + + License Agreement. + diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml index 2a4a3541..ec5c557e 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml @@ -9,6 +9,7 @@ xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager" xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser" xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" + xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700" x:DataType="viewModels:CheckpointBrowserViewModel" d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}" @@ -149,7 +150,7 @@ Margin="0,8,0,0"> - + @@ -162,7 +163,7 @@ IsEnabled="{Binding !IsImporting}" Command="{Binding ShowVersionDialogCommand}" CommandParameter="{Binding CivitModel}" - Content="All Versions" /> + Content="{x:Static lang:Resources.Label_AllVersions}" /> @@ -177,7 +178,7 @@ diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index 860f77b3..18844013 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -54,11 +54,11 @@ + Text="{x:Static lang:Resources.Action_Rename}" IconSource="Rename" /> + Text="{x:Static lang:Resources.Action_Delete}" IconSource="Delete" /> @@ -135,7 +135,7 @@ + ToolTip.Tip="{x:Static lang:Resources.Label_ConnectedModel}" /> - - - + - @@ -284,7 +284,7 @@ Effect="{StaticResource TextDropShadowEffect}" FontSize="24" HorizontalAlignment="Center" - Text="Drop file here to import" + Text="{x:Static lang:Resources.Label_DropFileToImport}" VerticalAlignment="Center" IsVisible="{Binding IsCurrentDragTarget}" /> + ToolTip.Tip="{x:Static lang:Resources.Label_ImportAsConnectedExplanation}" /> @@ -382,15 +382,15 @@ BorderThickness="4" IsIndeterminate="True" IsVisible="{Binding IsIndexing}"/> - diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml index 80e41576..f3912584 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml @@ -5,6 +5,7 @@ xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" + xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" d:DataContext="{x:Static mocks:DesignData.ExceptionViewModel}" x:DataType="dialogs:ExceptionViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550" @@ -21,7 +22,7 @@ Grid.Row="0" Theme="{DynamicResource SubtitleTextBlockStyle}" Margin="16" - Text="An unexpected error occured" + Text="{x:Static lang:Resources.Label_UnexpectedErrorOccurred}" TextWrapping="Wrap" VerticalAlignment="Top" /> @@ -74,7 +75,7 @@ @@ -164,7 +165,7 @@ @@ -179,14 +180,14 @@ Margin="0,8,0,0" HorizontalAlignment="Center"> @@ -58,55 +59,59 @@ public partial class CheckpointFolder : ObservableObject [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))] private bool isImportInProgress; - + public bool IsDragBlurEnabled => IsCurrentDragTarget || IsImportInProgress; - public string TitleWithFilesCount => CheckpointFiles.Any() ? $"{Title} ({CheckpointFiles.Count})" : Title; - + public string TitleWithFilesCount => + CheckpointFiles.Any() ? $"{Title} ({CheckpointFiles.Count})" : Title; + public ProgressViewModel Progress { get; } = new(); public ObservableCollection SubFolders { get; init; } = new(); public ObservableCollection CheckpointFiles { get; init; } = new(); - + public RelayCommand OnPreviewDragEnterCommand => new(() => IsCurrentDragTarget = true); public RelayCommand OnPreviewDragLeaveCommand => new(() => IsCurrentDragTarget = false); public CheckpointFolder( - IDialogFactory dialogFactory, + IDialogFactory dialogFactory, ISettingsManager settingsManager, IDownloadService downloadService, ModelFinder modelFinder, - bool useCategoryVisibility = true) + bool useCategoryVisibility = true + ) { this.dialogFactory = dialogFactory; this.settingsManager = settingsManager; this.downloadService = downloadService; this.modelFinder = modelFinder; this.useCategoryVisibility = useCategoryVisibility; - + CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged; } - + /// /// When title is set, set the category enabled state from settings. /// // ReSharper disable once UnusedParameterInPartialMethod partial void OnTitleChanged(string value) { - if (!useCategoryVisibility) return; - + if (!useCategoryVisibility) + return; + // Update folder type var result = Enum.TryParse(Title, out SharedFolderType type); FolderType = result ? type : new SharedFolderType(); - + IsCategoryEnabled = settingsManager.IsSharedFolderCategoryVisible(FolderType); } - + /// /// When toggling the category enabled state, save it to settings. /// partial void OnIsCategoryEnabledChanged(bool value) { - if (!useCategoryVisibility) return; + if (!useCategoryVisibility) + return; if (value != settingsManager.IsSharedFolderCategoryVisible(FolderType)) { settingsManager.SetSharedFolderCategoryVisible(FolderType, value); @@ -117,7 +122,8 @@ public partial class CheckpointFolder : ObservableObject private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(TitleWithFilesCount)); - if (e.NewItems == null) return; + if (e.NewItems == null) + return; // On new added items, add event handler for deletion foreach (CheckpointFile item in e.NewItems) { @@ -155,7 +161,7 @@ public partial class CheckpointFolder : ObservableObject { Process.Start("explorer.exe", path); } - + /// /// Imports files to the folder. Reports progress to instance properties. /// @@ -163,25 +169,30 @@ public partial class CheckpointFolder : ObservableObject { Progress.IsIndeterminate = true; Progress.IsProgressVisible = true; - var copyPaths = files.ToDictionary(k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v))); - + var copyPaths = files.ToDictionary( + k => k, + v => Path.Combine(DirectoryPath, Path.GetFileName(v)) + ); + var progress = new Progress(report => { Progress.IsIndeterminate = false; Progress.Value = report.Percentage; // For multiple files, add count - Progress.Text = copyPaths.Count > 1 ? $"Importing {report.Title} ({report.Message})" : $"Importing {report.Title}"; + Progress.Text = + copyPaths.Count > 1 + ? $"Importing {report.Title} ({report.Message})" + : $"Importing {report.Title}"; }); await FileTransfers.CopyFiles(copyPaths, progress); - + // Hash files and convert them to connected model if found if (convertToConnected) { var modelFilesCount = copyPaths.Count; - var modelFiles = copyPaths.Values - .Select(path => new FilePath(path)); - + var modelFiles = copyPaths.Values.Select(path => new FilePath(path)); + // Holds tasks for model queries after hash var modelQueryTasks = new List>(); @@ -191,27 +202,42 @@ public partial class CheckpointFolder : ObservableObject { Progress.IsIndeterminate = false; Progress.Value = report.Percentage; - Progress.Text = modelFilesCount > 1 ? - $"Computing metadata for {modelFile.Info.Name} ({i}/{modelFilesCount})" : - $"Computing metadata for {report.Title}"; + Progress.Text = + modelFilesCount > 1 + ? $"Computing metadata for {modelFile.Info.Name} ({i}/{modelFilesCount})" + : $"Computing metadata for {report.Title}"; }); - + var hashBlake3 = await FileHash.GetBlake3Async(modelFile, hashProgress); - + + if (!string.IsNullOrWhiteSpace(hashBlake3)) + { + settingsManager.Transaction(s => + { + s.InstalledModelHashes ??= new HashSet(); + s.InstalledModelHashes.Add(hashBlake3); + }); + } + // Start a task to query the model in background var queryTask = Task.Run(async () => { var result = await modelFinder.LocalFindModel(hashBlake3); result ??= await modelFinder.RemoteFindModel(hashBlake3); - if (result is null) return false; // Not found + if (result is null) + return false; // Not found var (model, version, file) = result.Value; - + // Save connected model info json var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name); var modelInfo = new ConnectedModelInfo( - model, version, file, DateTimeOffset.UtcNow); + model, + version, + file, + DateTimeOffset.UtcNow + ); await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName); // If available, save thumbnail @@ -222,7 +248,8 @@ public partial class CheckpointFolder : ObservableObject if (imageExt is "jpg" or "jpeg" or "png") { var imageDownloadPath = Path.GetFullPath( - Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}")); + Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}") + ); await downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); } } @@ -231,30 +258,29 @@ public partial class CheckpointFolder : ObservableObject }); modelQueryTasks.Add(queryTask); } - + // Set progress to indeterminate Progress.IsIndeterminate = true; Progress.Text = "Checking connected model information"; - + // Wait for all model queries to finish var modelQueryResults = await Task.WhenAll(modelQueryTasks); - + var successCount = modelQueryResults.Count(r => r); var totalCount = modelQueryResults.Length; var failCount = totalCount - successCount; await IndexAsync(); - + Progress.Value = 100; Progress.Text = successCount switch { - 0 when failCount > 0 => - "Import complete. No connected data found.", - > 0 when failCount > 0 => - $"Import complete. Found connected data for {successCount} of {totalCount} models.", + 0 when failCount > 0 => "Import complete. No connected data found.", + > 0 when failCount > 0 + => $"Import complete. Found connected data for {successCount} of {totalCount} models.", _ => $"Import complete. Found connected data for all {totalCount} models." }; - + DelayedClearProgress(TimeSpan.FromSeconds(1)); } else @@ -271,33 +297,46 @@ public partial class CheckpointFolder : ObservableObject /// private void DelayedClearProgress(TimeSpan delay) { - Task.Delay(delay).ContinueWith(_ => - { - IsImportInProgress = false; - Progress.IsProgressVisible = false; - Progress.Value = 0; - Progress.Text = string.Empty; - }); + Task.Delay(delay) + .ContinueWith(_ => + { + IsImportInProgress = false; + Progress.IsProgressVisible = false; + Progress.Value = 0; + Progress.Text = string.Empty; + }); } - + /// /// Gets checkpoint files from folder index /// - private async Task> GetCheckpointFilesAsync(IProgress? progress = default) + private async Task> GetCheckpointFilesAsync( + IProgress? progress = default + ) { if (!Directory.Exists(DirectoryPath)) { return new List(); } - return await (progress switch - { - null => Task.Run(() => - CheckpointFile.FromDirectoryIndex(dialogFactory, DirectoryPath).ToList()), - - _ => Task.Run(() => - CheckpointFile.FromDirectoryIndex(dialogFactory, DirectoryPath, progress).ToList()) - }); + return await ( + progress switch + { + null + => Task.Run( + () => + CheckpointFile.FromDirectoryIndex(dialogFactory, DirectoryPath).ToList() + ), + + _ + => Task.Run( + () => + CheckpointFile + .FromDirectoryIndex(dialogFactory, DirectoryPath, progress) + .ToList() + ) + } + ); } /// @@ -309,19 +348,23 @@ public partial class CheckpointFolder : ObservableObject foreach (var folder in Directory.GetDirectories(DirectoryPath)) { // Inherit our folder type - var subFolder = new CheckpointFolder(dialogFactory, settingsManager, - downloadService, modelFinder, - useCategoryVisibility: false) + var subFolder = new CheckpointFolder( + dialogFactory, + settingsManager, + downloadService, + modelFinder, + useCategoryVisibility: false + ) { Title = Path.GetFileName(folder), DirectoryPath = folder, FolderType = FolderType }; - + await subFolder.IndexAsync(progress); SubFolders.Add(subFolder); } - + var checkpointFiles = await GetCheckpointFilesAsync(); CheckpointFiles.Clear(); foreach (var checkpointFile in checkpointFiles) From 9b597935b08442d21985a6746d36d79c6aa77018 Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 10 Sep 2023 19:07:13 -0700 Subject: [PATCH 24/29] Add toggle for "Show Model Images" on checkpoints page & add "find connected metadata" option in CheckpointFolder context menu --- CHANGELOG.md | 2 + .../DesignData/DesignData.cs | 8 +- .../Languages/Resources.Designer.cs | 18 ++ .../Languages/Resources.resx | 6 + .../CheckpointManager/CheckpointFile.cs | 138 ++++++--- .../CheckpointManager/CheckpointFolder.cs | 263 ++++++++++++------ .../ViewModels/CheckpointsPageViewModel.cs | 129 ++++++--- .../Views/CheckpointsPage.axaml | 45 ++- .../Models/Settings/Settings.cs | 29 +- StabilityMatrix/Models/CheckpointFolder.cs | 11 +- 10 files changed, 443 insertions(+), 206 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 179d26d2..047875ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 ## v2.4.0 ### Added - New installable Package - [Fooocus-MRE](https://github.com/MoonRide303/Fooocus-MRE) +- Added toggle to show connected model images in the Checkpoints tab +- Added "Find Connected Metadata" option to the context menu of Checkpoint Folders in the Checkpoints tab to connect models that don't have any metadata ### Changed - Revamped package installer - Added "advanced options" section for commit, shared folder method, and pytorch options diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index b34ed33e..914ce421 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -168,7 +168,7 @@ public static class DesignData // Checkpoints page CheckpointsPageViewModel.CheckpointFolders = new ObservableCollection { - new(settingsManager, downloadService, modelFinder) + new(settingsManager, downloadService, modelFinder, notificationService) { Title = "StableDiffusion", DirectoryPath = "Models/StableDiffusion", @@ -198,18 +198,18 @@ public static class DesignData new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }, }, }, - new(settingsManager, downloadService, modelFinder) + new(settingsManager, downloadService, modelFinder, notificationService) { Title = "Lora", DirectoryPath = "Packages/Lora", SubFolders = new AdvancedObservableList() { - new(settingsManager, downloadService, modelFinder) + new(settingsManager, downloadService, modelFinder, notificationService) { Title = "StableDiffusion", DirectoryPath = "Packages/Lora/Subfolder", }, - new(settingsManager, downloadService, modelFinder) + new(settingsManager, downloadService, modelFinder, notificationService) { Title = "Lora", DirectoryPath = "Packages/StableDiffusion/Subfolder", diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index f27f2232..d9fcb10d 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -365,6 +365,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Find Connected Metadata. + /// + public static string Label_FindConnectedMetadata { + get { + return ResourceManager.GetString("Label_FindConnectedMetadata", resourceCulture); + } + } + /// /// Looks up a localized string similar to First Page. /// @@ -653,6 +662,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Show Model Images. + /// + public static string Label_ShowModelImages { + get { + return ResourceManager.GetString("Label_ShowModelImages", resourceCulture); + } + } + /// /// Looks up a localized string similar to Show NSFW Content. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index a825e8bb..af9a85a4 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -246,4 +246,10 @@ License Agreement. + + Find Connected Metadata + + + Show Model Images + diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs index 84e059f3..0245ef12 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia.Data; +using Blake3; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; @@ -45,14 +46,24 @@ public partial class CheckpointFile : ViewModelBase private ConnectedModelInfo? connectedModel; public bool IsConnectedModel => ConnectedModel != null; - [ObservableProperty] private bool isLoading; - [ObservableProperty] private CivitModelType modelType; - + [ObservableProperty] + private bool isLoading; + + [ObservableProperty] + private CivitModelType modelType; + 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" }; @@ -78,10 +89,11 @@ public partial class CheckpointFile : ViewModelBase if (string.IsNullOrEmpty(FilePath)) { throw new InvalidOperationException( - "Cannot get connected model info file path when FilePath is empty"); + "Cannot get connected model info file path when FilePath is empty" + ); } - var modelNameNoExt = Path.GetFileNameWithoutExtension((string?) FilePath); - var modelDir = Path.GetDirectoryName((string?) FilePath) ?? ""; + var modelNameNoExt = Path.GetFileNameWithoutExtension((string?)FilePath); + var modelDir = Path.GetDirectoryName((string?)FilePath) ?? ""; return Path.Combine(modelDir, $"{modelNameNoExt}.cm-info.json"); } @@ -125,7 +137,7 @@ public partial class CheckpointFile : ViewModelBase private async Task RenameAsync() { // Parent folder path - var parentPath = Path.GetDirectoryName((string?) FilePath) ?? ""; + var parentPath = Path.GetDirectoryName((string?)FilePath) ?? ""; var textFields = new TextBoxField[] { @@ -134,18 +146,18 @@ public partial class CheckpointFile : ViewModelBase Label = "File name", Validator = text => { - if (string.IsNullOrWhiteSpace(text)) throw new - DataValidationException("File name is required"); - - if (File.Exists(Path.Combine(parentPath, text))) throw new - DataValidationException("File name already exists"); + if (string.IsNullOrWhiteSpace(text)) + throw new DataValidationException("File name is required"); + + if (File.Exists(Path.Combine(parentPath, text))) + throw new DataValidationException("File name already exists"); }, Text = FileName } }; var dialog = DialogHelper.CreateTextEntryDialog("Rename Model", "", textFields); - + if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var name = textFields[0].Text; @@ -160,7 +172,10 @@ public partial class CheckpointFile : ViewModelBase // If preview image exists, rename it too if (PreviewImagePath != null && File.Exists(PreviewImagePath)) { - var newPreviewImagePath = Path.Combine(parentPath, $"{nameNoExt}.preview{Path.GetExtension((string?) PreviewImagePath)}"); + var newPreviewImagePath = Path.Combine( + parentPath, + $"{nameNoExt}.preview{Path.GetExtension((string?)PreviewImagePath)}" + ); File.Move(PreviewImagePath, newPreviewImagePath); PreviewImagePath = newPreviewImagePath; } @@ -170,7 +185,10 @@ public partial class CheckpointFile : ViewModelBase var cmInfoPath = Path.Combine(parentPath, $"{originalNameNoExt}.cm-info.json"); if (File.Exists(cmInfoPath)) { - File.Move(cmInfoPath, Path.Combine(parentPath, $"{nameNoExt}.cm-info.json")); + File.Move( + cmInfoPath, + Path.Combine(parentPath, $"{nameNoExt}.cm-info.json") + ); } } } @@ -184,7 +202,8 @@ public partial class CheckpointFile : ViewModelBase [RelayCommand] private void OpenOnCivitAi() { - if (ConnectedModel?.ModelId == null) return; + if (ConnectedModel?.ModelId == null) + return; ProcessRunner.OpenUrl($"https://civitai.com/models/{ConnectedModel.ModelId}"); } @@ -195,22 +214,33 @@ public partial class CheckpointFile : ViewModelBase /// - {filename}.preview.{image-extensions} (preview image) /// - {filename}.cm-info.json (connected model info) /// - public static IEnumerable FromDirectoryIndex(string directory, SearchOption searchOption = SearchOption.TopDirectoryOnly) + public static IEnumerable FromDirectoryIndex( + string directory, + SearchOption searchOption = SearchOption.TopDirectoryOnly + ) { foreach (var file in Directory.EnumerateFiles(directory, "*.*", searchOption)) { - if (!SupportedCheckpointExtensions.Any(ext => - Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase))) + if ( + !SupportedCheckpointExtensions.Any( + ext => + Path.GetExtension(file) + .Equals(ext, StringComparison.InvariantCultureIgnoreCase) + ) + ) continue; - + var checkpointFile = new CheckpointFile { Title = Path.GetFileNameWithoutExtension(file), FilePath = Path.Combine(directory, file), }; - - var jsonPath = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.cm-info.json"); - if (File.Exists(jsonPath)) + + var jsonPath = Path.Combine( + directory, + $"{Path.GetFileNameWithoutExtension(file)}.cm-info.json" + ); + if (File.Exists(jsonPath)) { var json = File.ReadAllText(jsonPath); var connectedModelInfo = ConnectedModelInfo.FromJson(json); @@ -218,32 +248,51 @@ public partial class CheckpointFile : ViewModelBase } checkpointFile.PreviewImagePath = SupportedImageExtensions - .Select(ext => Path.Combine(directory, - $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists) + .Select( + ext => + Path.Combine( + directory, + $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}" + ) + ) + .Where(File.Exists) .FirstOrDefault(); yield return checkpointFile; } } - + public static IEnumerable GetAllCheckpointFiles(string modelsDirectory) { - foreach (var file in Directory.EnumerateFiles(modelsDirectory, "*.*", SearchOption.AllDirectories)) + foreach ( + var file in Directory.EnumerateFiles( + modelsDirectory, + "*.*", + SearchOption.AllDirectories + ) + ) { - if (!SupportedCheckpointExtensions.Any(ext => - Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase))) + if ( + !SupportedCheckpointExtensions.Any( + ext => + Path.GetExtension(file) + .Equals(ext, StringComparison.InvariantCultureIgnoreCase) + ) + ) continue; - + var checkpointFile = new CheckpointFile { Title = Path.GetFileNameWithoutExtension(file), FilePath = file, }; - var jsonPath = Path.Combine(Path.GetDirectoryName(file) ?? "", - Path.GetFileNameWithoutExtension(file) + ".cm-info.json"); - - if (File.Exists(jsonPath)) + var jsonPath = Path.Combine( + Path.GetDirectoryName(file) ?? "", + Path.GetFileNameWithoutExtension(file) + ".cm-info.json" + ); + + if (File.Exists(jsonPath)) { var json = File.ReadAllText(jsonPath); var connectedModelInfo = ConnectedModelInfo.FromJson(json); @@ -252,8 +301,14 @@ public partial class CheckpointFile : ViewModelBase } checkpointFile.PreviewImagePath = SupportedImageExtensions - .Select(ext => Path.Combine(Path.GetDirectoryName(file) ?? "", - $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists) + .Select( + ext => + Path.Combine( + Path.GetDirectoryName(file) ?? "", + $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}" + ) + ) + .Where(File.Exists) .FirstOrDefault(); yield return checkpointFile; @@ -263,8 +318,11 @@ public partial class CheckpointFile : ViewModelBase /// /// Index with progress reporting. /// - public static IEnumerable FromDirectoryIndex(string directory, IProgress progress, - SearchOption searchOption = SearchOption.TopDirectoryOnly) + public static IEnumerable FromDirectoryIndex( + string directory, + IProgress progress, + SearchOption searchOption = SearchOption.TopDirectoryOnly + ) { var current = 0ul; foreach (var checkpointFile in FromDirectoryIndex(directory, searchOption)) @@ -281,7 +339,7 @@ public partial class CheckpointFile : ViewModelBase { return CivitModelType.Checkpoint; } - + if (filePath.Contains(SharedFolderType.ControlNet.ToString())) { return CivitModelType.Controlnet; diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs index c7d51c5e..2db712ea 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Platform.Storage; using Avalonia.Threading; @@ -12,6 +13,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; @@ -28,9 +30,11 @@ public partial class CheckpointFolder : ViewModelBase private readonly ISettingsManager settingsManager; private readonly IDownloadService downloadService; private readonly ModelFinder modelFinder; + private readonly INotificationService notificationService; + // ReSharper disable once FieldCanBeMadeReadOnly.Local private bool useCategoryVisibility; - + /// /// Absolute path to the folder. /// @@ -50,12 +54,14 @@ public partial class CheckpointFolder : ViewModelBase /// /// True if the category is enabled for the manager page. /// - [ObservableProperty] private bool isCategoryEnabled = true; + [ObservableProperty] + private bool isCategoryEnabled = true; /// /// True if currently expanded in the UI. /// - [ObservableProperty] private bool isExpanded = true; + [ObservableProperty] + private bool isExpanded = true; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))] @@ -65,15 +71,16 @@ public partial class CheckpointFolder : ViewModelBase [NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))] private bool isImportInProgress; - [ObservableProperty] private string searchFilter = string.Empty; - + [ObservableProperty] + private string searchFilter = string.Empty; + public bool IsDragBlurEnabled => IsCurrentDragTarget || IsImportInProgress; public string TitleWithFilesCount => CheckpointFiles.Any() || SubFolders.Any(f => f.CheckpointFiles.Any()) ? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})" : Title; - + public ProgressViewModel Progress { get; } = new(); public CheckpointFolder? ParentFolder { get; init; } @@ -85,29 +92,33 @@ public partial class CheckpointFolder : ViewModelBase ISettingsManager settingsManager, IDownloadService downloadService, ModelFinder modelFinder, - bool useCategoryVisibility = true) + INotificationService notificationService, + bool useCategoryVisibility = true + ) { this.settingsManager = settingsManager; this.downloadService = downloadService; this.modelFinder = modelFinder; + this.notificationService = notificationService; this.useCategoryVisibility = useCategoryVisibility; - + CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged; DisplayedCheckpointFiles = CheckpointFiles; } - + /// /// When title is set, set the category enabled state from settings. /// // ReSharper disable once UnusedParameterInPartialMethod partial void OnTitleChanged(string value) { - if (!useCategoryVisibility) return; - + if (!useCategoryVisibility) + return; + // Update folder type var result = Enum.TryParse(Title, out SharedFolderType type); FolderType = result ? type : new SharedFolderType(); - + IsCategoryEnabled = settingsManager.IsSharedFolderCategoryVisible(FolderType); } @@ -119,8 +130,9 @@ public partial class CheckpointFolder : ViewModelBase } else { - var filteredFiles = CheckpointFiles.Where(y => - y.FileName.Contains(value, StringComparison.OrdinalIgnoreCase)); + var filteredFiles = CheckpointFiles.Where( + y => y.FileName.Contains(value, StringComparison.OrdinalIgnoreCase) + ); DisplayedCheckpointFiles = new AdvancedObservableList(filteredFiles); } } @@ -130,13 +142,14 @@ public partial class CheckpointFolder : ViewModelBase /// partial void OnIsCategoryEnabledChanged(bool value) { - if (!useCategoryVisibility) return; + if (!useCategoryVisibility) + return; if (value != settingsManager.IsSharedFolderCategoryVisible(FolderType)) { settingsManager.SetSharedFolderCategoryVisible(FolderType, value); } } - + private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(TitleWithFilesCount)); @@ -168,7 +181,7 @@ public partial class CheckpointFolder : ViewModelBase { await ProcessRunner.OpenFolderBrowser(path); } - + [RelayCommand] private async Task Delete() { @@ -181,7 +194,9 @@ public partial class CheckpointFolder : ViewModelBase } var dialog = DialogHelper.CreateTaskDialog( - "Are you sure you want to delete this folder?",directory); + "Are you sure you want to delete this folder?", + directory + ); dialog.ShowProgressBar = false; dialog.Buttons = new List @@ -189,7 +204,7 @@ public partial class CheckpointFolder : ViewModelBase TaskDialogButton.YesButton, TaskDialogButton.NoButton }; - + dialog.Closing += async (sender, e) => { // We only want to use the deferral on the 'Yes' Button @@ -204,7 +219,7 @@ public partial class CheckpointFolder : ViewModelBase { await directory.DeleteAsync(true); } - + RemoveFromParentList(); deferral.Complete(); } @@ -214,12 +229,12 @@ public partial class CheckpointFolder : ViewModelBase await dialog.ShowAsync(true); } - + [RelayCommand] private async Task CreateSubFolder() { Dispatcher.UIThread.VerifyAccess(); - + var textBox = new TextBox(); var dialog = new ContentDialog { @@ -235,52 +250,71 @@ public partial class CheckpointFolder : ViewModelBase if (result == ContentDialogResult.Primary) { var targetName = textBox.Text; - if (string.IsNullOrWhiteSpace(targetName)) return; - + if (string.IsNullOrWhiteSpace(targetName)) + return; + var subFolderPath = Path.Combine(DirectoryPath, targetName); - + Directory.CreateDirectory(subFolderPath); - - SubFolders.Add(new CheckpointFolder(settingsManager, - downloadService, modelFinder, - useCategoryVisibility: false) - { - Title = Path.GetFileName(subFolderPath), - DirectoryPath = subFolderPath, - FolderType = FolderType, - ParentFolder = this, - IsExpanded = false, - }); + + SubFolders.Add( + new CheckpointFolder( + settingsManager, + downloadService, + modelFinder, + notificationService, + useCategoryVisibility: false + ) + { + Title = Path.GetFileName(subFolderPath), + DirectoryPath = subFolderPath, + FolderType = FolderType, + ParentFolder = this, + IsExpanded = false, + } + ); } } - + /// /// Imports files to the folder. Reports progress to instance properties. /// - public async Task ImportFilesAsync(IEnumerable files, bool convertToConnected = false) + public async Task ImportFilesAsync( + IEnumerable files, + bool convertToConnected = false, + bool copyFiles = true + ) { try { Progress.Value = 0; - var copyPaths = files.ToDictionary(k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v))); - + var copyPaths = files.ToDictionary( + k => k, + v => Path.Combine(DirectoryPath, Path.GetFileName(v)) + ); + var progress = new Progress(report => { Progress.IsIndeterminate = false; Progress.Value = report.Percentage; // For multiple files, add count - Progress.Text = copyPaths.Count > 1 ? $"Importing {report.Title} ({report.Message})" : $"Importing {report.Title}"; + Progress.Text = + copyPaths.Count > 1 + ? $"Importing {report.Title} ({report.Message})" + : $"Importing {report.Title}"; }); - await FileTransfers.CopyFiles(copyPaths, progress); - + if (copyFiles) + { + await FileTransfers.CopyFiles(copyPaths, progress); + } + // Hash files and convert them to connected model if found if (convertToConnected) { var modelFilesCount = copyPaths.Count; - var modelFiles = copyPaths.Values - .Select(path => new FilePath(path)); - + var modelFiles = copyPaths.Values.Select(path => new FilePath(path)); + // Holds tasks for model queries after hash var modelQueryTasks = new List>(); @@ -290,27 +324,33 @@ public partial class CheckpointFolder : ViewModelBase { Progress.IsIndeterminate = report.IsIndeterminate; Progress.Value = report.Percentage; - Progress.Text = modelFilesCount > 1 ? - $"Computing metadata for {modelFile.Name} ({i}/{modelFilesCount})" : - $"Computing metadata for {modelFile.Name}"; + Progress.Text = + modelFilesCount > 1 + ? $"Computing metadata for {modelFile.Name} ({i}/{modelFilesCount})" + : $"Computing metadata for {modelFile.Name}"; }); - + var hashBlake3 = await FileHash.GetBlake3Async(modelFile, hashProgress); - + // Start a task to query the model in background var queryTask = Task.Run(async () => { var result = await modelFinder.LocalFindModel(hashBlake3); result ??= await modelFinder.RemoteFindModel(hashBlake3); - if (result is null) return false; // Not found + if (result is null) + return false; // Not found var (model, version, file) = result.Value; - + // Save connected model info json var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name); var modelInfo = new ConnectedModelInfo( - model, version, file, DateTimeOffset.UtcNow); + model, + version, + file, + DateTimeOffset.UtcNow + ); await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName); // If available, save thumbnail @@ -321,8 +361,15 @@ public partial class CheckpointFolder : ViewModelBase if (imageExt is "jpg" or "jpeg" or "png") { var imageDownloadPath = Path.GetFullPath( - Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}")); - await downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); + Path.Combine( + DirectoryPath, + $"{modelFileName}.preview.{imageExt}" + ) + ); + await downloadService.DownloadToFileAsync( + image.Url, + imageDownloadPath + ); } } @@ -330,29 +377,27 @@ public partial class CheckpointFolder : ViewModelBase }); modelQueryTasks.Add(queryTask); } - + // Set progress to indeterminate Progress.IsIndeterminate = true; Progress.Text = "Checking connected model information"; - + // Wait for all model queries to finish var modelQueryResults = await Task.WhenAll(modelQueryTasks); - + var successCount = modelQueryResults.Count(r => r); var totalCount = modelQueryResults.Length; var failCount = totalCount - successCount; await IndexAsync(); - + Progress.Value = 100; Progress.Text = successCount switch { - 0 when failCount > 0 => - "Import complete. No connected data found.", - > 0 when failCount > 0 => - $"Import complete. Found connected data for {successCount} of {totalCount} models.", - 1 when failCount == 0 => - "Import complete. Found connected data for 1 model.", + 0 when failCount > 0 => "Import complete. No connected data found.", + > 0 when failCount > 0 + => $"Import complete. Found connected data for {successCount} of {totalCount} models.", + 1 when failCount == 0 => "Import complete. Found connected data for 1 model.", _ => $"Import complete. Found connected data for all {totalCount} models." }; } @@ -369,38 +414,73 @@ public partial class CheckpointFolder : ViewModelBase } } + public async Task FindConnectedMetadata() + { + try + { + IsImportInProgress = true; + var files = CheckpointFiles + .Where(f => !f.IsConnectedModel) + .Select(f => f.FilePath) + .ToList(); + + if (files.Any()) + { + await ImportFilesAsync(files, true, false); + } + else + { + notificationService.Show( + "Cannot Find Connected Metadata", + "All files in this folder are already connected.", + NotificationType.Warning + ); + } + } + finally + { + IsImportInProgress = false; + } + } + /// /// Clears progress after a delay. /// private void DelayedClearProgress(TimeSpan delay) { - Task.Delay(delay).ContinueWith(_ => - { - IsImportInProgress = false; - Progress.Value = 0; - Progress.IsIndeterminate = false; - Progress.Text = string.Empty; - }); + Task.Delay(delay) + .ContinueWith(_ => + { + IsImportInProgress = false; + Progress.Value = 0; + Progress.IsIndeterminate = false; + Progress.Text = string.Empty; + }); } - + /// /// Gets checkpoint files from folder index /// - private async Task> GetCheckpointFilesAsync(IProgress? progress = default) + private async Task> GetCheckpointFilesAsync( + IProgress? progress = default + ) { if (!Directory.Exists(DirectoryPath)) { return new List(); } - return await (progress switch - { - null => Task.Run(() => - CheckpointFile.FromDirectoryIndex(DirectoryPath).ToList()), - - _ => Task.Run(() => - CheckpointFile.FromDirectoryIndex(DirectoryPath, progress).ToList()) - }); + return await ( + progress switch + { + null => Task.Run(() => CheckpointFile.FromDirectoryIndex(DirectoryPath).ToList()), + + _ + => Task.Run( + () => CheckpointFile.FromDirectoryIndex(DirectoryPath, progress).ToList() + ) + } + ); } /// @@ -413,8 +493,13 @@ public partial class CheckpointFolder : ViewModelBase foreach (var folder in Directory.GetDirectories(DirectoryPath)) { // Create subfolder - var subFolder = new CheckpointFolder(settingsManager, - downloadService, modelFinder, useCategoryVisibility: false) + var subFolder = new CheckpointFolder( + settingsManager, + downloadService, + modelFinder, + notificationService, + useCategoryVisibility: false + ) { Title = Path.GetFileName(folder), DirectoryPath = folder, @@ -422,12 +507,14 @@ public partial class CheckpointFolder : ViewModelBase ParentFolder = this, IsExpanded = false, // Subfolders are collapsed by default }; - + await subFolder.IndexAsync(progress); SubFolders.Add(subFolder); } - + CheckpointFiles.Clear(); - CheckpointFiles.AddRange(await GetCheckpointFilesAsync()); + var files = await GetCheckpointFilesAsync(); + var orderedFiles = files.OrderByDescending(f => f.IsConnectedModel); + CheckpointFiles.AddRange(orderedFiles); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs index b3351ebc..954a4c4b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs @@ -10,6 +10,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using NLog; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.Views; @@ -30,24 +31,34 @@ public partial class CheckpointsPageViewModel : PageViewModelBase private readonly ISettingsManager settingsManager; private readonly ModelFinder modelFinder; private readonly IDownloadService downloadService; + private readonly INotificationService notificationService; public override string Title => "Checkpoints"; - public override IconSource IconSource => new SymbolIconSource - {Symbol = Symbol.Notebook, IsFilled = true}; + public override IconSource IconSource => + new SymbolIconSource { Symbol = Symbol.Notebook, IsFilled = true }; // Toggle button for auto hashing new drag-and-dropped files for connected upgrade - [ObservableProperty] private bool isImportAsConnected; - [ObservableProperty] private bool isLoading; - [ObservableProperty] private bool isIndexing; - + [ObservableProperty] + private bool isImportAsConnected; + + [ObservableProperty] + private bool isLoading; + + [ObservableProperty] + private bool isIndexing; + + [ObservableProperty] + private bool showConnectedModelImages; + [ObservableProperty] private string searchFilter = string.Empty; partial void OnIsImportAsConnectedChanged(bool value) { - if (settingsManager.IsLibraryDirSet && - value != settingsManager.Settings.IsImportAsConnected) + if ( + settingsManager.IsLibraryDirSet && value != settingsManager.Settings.IsImportAsConnected + ) { settingsManager.Transaction(s => s.IsImportAsConnected = value); } @@ -57,20 +68,24 @@ public partial class CheckpointsPageViewModel : PageViewModelBase private ObservableCollection checkpointFolders = new(); [ObservableProperty] - private ObservableCollection displayedCheckpointFolders = new(); + private ObservableCollection displayedCheckpointFolders = + new(); public CheckpointsPageViewModel( ISharedFolders sharedFolders, ISettingsManager settingsManager, IDownloadService downloadService, - ModelFinder modelFinder) + INotificationService notificationService, + ModelFinder modelFinder + ) { this.sharedFolders = sharedFolders; this.settingsManager = settingsManager; this.downloadService = downloadService; + this.notificationService = notificationService; this.modelFinder = modelFinder; } - + public override async Task OnLoadedAsync() { var sw = Stopwatch.StartNew(); @@ -78,19 +93,23 @@ public partial class CheckpointsPageViewModel : PageViewModelBase // Set UI states IsImportAsConnected = settingsManager.Settings.IsImportAsConnected; + ShowConnectedModelImages = settingsManager.Settings.ShowConnectedModelImages; // Refresh search filter OnSearchFilterChanged(string.Empty); - Logger.Info($"Loaded {DisplayedCheckpointFolders.Count} checkpoint folders in {sw.ElapsedMilliseconds}ms"); - - if (Design.IsDesignMode) return; + Logger.Info( + $"Loaded {DisplayedCheckpointFolders.Count} checkpoint folders in {sw.ElapsedMilliseconds}ms" + ); + + if (Design.IsDesignMode) + return; IsLoading = CheckpointFolders.Count == 0; IsIndexing = CheckpointFolders.Count > 0; await IndexFolders(); IsLoading = false; IsIndexing = false; - + Logger.Info($"OnLoadedAsync in {sw.ElapsedMilliseconds}ms"); } @@ -105,16 +124,16 @@ public partial class CheckpointsPageViewModel : PageViewModelBase { x.SearchFilter = SearchFilter; return x; - })); + }) + ); sw.Stop(); Logger.Info($"OnSearchFilterChanged in {sw.ElapsedMilliseconds}ms"); return; } - + sw.Restart(); - - var filteredFolders = CheckpointFolders - .Where(ContainsSearchFilter).ToList(); + + var filteredFolders = CheckpointFolders.Where(ContainsSearchFilter).ToList(); foreach (var folder in filteredFolders) { folder.SearchFilter = SearchFilter; @@ -124,17 +143,30 @@ public partial class CheckpointsPageViewModel : PageViewModelBase DisplayedCheckpointFolders = new ObservableCollection(filteredFolders); } - + + partial void OnShowConnectedModelImagesChanged(bool value) + { + if ( + settingsManager.IsLibraryDirSet + && value != settingsManager.Settings.ShowConnectedModelImages + ) + { + settingsManager.Transaction(s => s.ShowConnectedModelImages = value); + } + } + private bool ContainsSearchFilter(CheckpointManager.CheckpointFolder folder) { if (folder == null) throw new ArgumentNullException(nameof(folder)); // Check files in the current folder - return folder.CheckpointFiles.Any(x => - x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)) || - // If no matching files were found in the current folder, check in all subfolders - folder.SubFolders.Any(subFolder => ContainsSearchFilter(subFolder)); + return folder.CheckpointFiles.Any( + x => x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase) + ) + || + // If no matching files were found in the current folder, check in all subfolders + folder.SubFolders.Any(subFolder => ContainsSearchFilter(subFolder)); } private async Task IndexFolders() @@ -146,48 +178,53 @@ public partial class CheckpointsPageViewModel : PageViewModelBase CheckpointFolders.Clear(); return; } - + // Setup shared folders in case they're missing sharedFolders.SetupSharedModelFolders(); var folders = Directory.GetDirectories(modelsDirectory); var sw = Stopwatch.StartNew(); - + // Index all folders - var indexTasks = folders.Select(async f => - { - var checkpointFolder = - new CheckpointFolder(settingsManager, downloadService, modelFinder) + var indexTasks = folders + .Select(async f => + { + var checkpointFolder = new CheckpointFolder( + settingsManager, + downloadService, + modelFinder, + notificationService + ) { Title = Path.GetFileName(f), DirectoryPath = f, IsExpanded = true, // Top level folders expanded by default }; - await checkpointFolder.IndexAsync(); - return checkpointFolder; - }).ToList(); + await checkpointFolder.IndexAsync(); + return checkpointFolder; + }) + .ToList(); await Task.WhenAll(indexTasks); - + sw.Stop(); Logger.Info($"IndexFolders in {sw.ElapsedMilliseconds}ms"); // Set new observable collection, ordered by alphabetical order - CheckpointFolders = - new ObservableCollection(indexTasks - .Select(t => t.Result) - .OrderBy(f => f.Title)); - + CheckpointFolders = new ObservableCollection( + indexTasks.Select(t => t.Result).OrderBy(f => f.Title) + ); + if (!string.IsNullOrWhiteSpace(SearchFilter)) { var filtered = CheckpointFolders - .Where(x => x.CheckpointFiles.Any(y => y.FileName.Contains(SearchFilter))).Select( - f => - { - f.SearchFilter = SearchFilter; - return f; - }); + .Where(x => x.CheckpointFiles.Any(y => y.FileName.Contains(SearchFilter))) + .Select(f => + { + f.SearchFilter = SearchFilter; + return f; + }); DisplayedCheckpointFolders = new ObservableCollection(filtered); } else diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index 18844013..e1f8e17f 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -65,12 +65,12 @@ - + @@ -119,6 +119,16 @@ TextWrapping="WrapWithOverflow" IsVisible="{Binding IsConnectedModel}" /> + + +