From 94115fd7007391dcf06c9be63794c8dbcf78676a Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 20 Aug 2023 16:01:08 -0700 Subject: [PATCH 01/60] Added Fooocus & select new data directory button --- CHANGELOG.md | 4 + StabilityMatrix.Avalonia/App.axaml.cs | 1 + .../StabilityMatrix.Avalonia.csproj | 2 +- .../Dialogs/SelectModelVersionViewModel.cs | 7 +- .../ViewModels/SettingsViewModel.cs | 36 +++++ .../Views/SettingsPage.axaml | 43 ++++-- StabilityMatrix.Core/Helper/HardwareHelper.cs | 10 ++ .../Models/Packages/Fooocus.cs | 124 ++++++++++++++++++ .../Models/Packages/VladAutomatic.cs | 18 +-- 9 files changed, 214 insertions(+), 31 deletions(-) create mode 100644 StabilityMatrix.Core/Models/Packages/Fooocus.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 764ee032..ebe4364c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ 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.2.1 +### Added +- New installable Package - [Fooocus](https://github.com/lllyasviel/Fooocus) +- Added "Select New Data Directory" button to Settings + ### Fixed - Fixed SD.Next shared folders config not working with new config format, reverted to Junctions / Symlinks diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 382f7274..487b73a0 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -308,6 +308,7 @@ public sealed class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } private static IServiceCollection ConfigureServices() diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 2acc5271..f9d7a498 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -8,7 +8,7 @@ app.manifest true ./Assets/Icon.ico - 2.1.1-dev.1 + 2.3.0-dev.1 $(Version) true diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs index d6435c85..082859db 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs @@ -42,8 +42,11 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase var firstImageUrl = value?.ModelVersion?.Images?.FirstOrDefault( img => nsfwEnabled || img.Nsfw == "None")?.Url; - Dispatcher.UIThread.InvokeAsync(async - () => await UpdateImage(firstImageUrl)); + Dispatcher.UIThread.InvokeAsync(async () => + { + SelectedFile = value?.CivitFileViewModels.FirstOrDefault(); + await UpdateImage(firstImageUrl); + }); } partial void OnSelectedFileChanged(CivitFileViewModel? value) diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 54026801..7c013f2e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -329,6 +329,42 @@ public partial class SettingsViewModel : PageViewModelBase "Stability Matrix has been added to the Start Menu for all users.", NotificationType.Success); } + public async Task PickNewDataDirectory() + { + var viewModel = dialogFactory.Get(); + var dialog = new BetterContentDialog + { + IsPrimaryButtonEnabled = false, + IsSecondaryButtonEnabled = false, + IsFooterVisible = false, + Content = new SelectDataDirectoryDialog + { + DataContext = viewModel + } + }; + + var result = await dialog.ShowAsync(); + if (result == ContentDialogResult.Primary) + { + // 1. For portable mode, call settings.SetPortableMode() + if (viewModel.IsPortableMode) + { + settingsManager.SetPortableMode(); + } + // 2. For custom path, call settings.SetLibraryPath(path) + else + { + settingsManager.SetLibraryPath(viewModel.DataDirectory); + } + + // Try to find library again, should be found now + if (!settingsManager.TryFindLibrary()) + { + throw new Exception("Could not find library after setting path"); + } + } + } + #endregion #region Debug Section diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index 8e3d6d0d..01443d17 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -6,6 +6,7 @@ xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" + xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700" x:DataType="vm:SettingsViewModel" x:CompileBindings="True" @@ -124,7 +125,7 @@ - + + + + + + @@ -181,24 +200,24 @@ IconSource="Code" Command="{Binding LoadDebugInfo}" Header="Debug Options" - Margin="8, 0,8,4"> + Margin="8, 0,8,0"> + Margin="4, 0"> + Margin="4,0"> + Margin="4,0"> @@ -206,10 +225,9 @@ + Margin="4,0"> - @@ -223,30 +241,27 @@ + Margin="4,0"> + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + @@ -51,23 +161,11 @@ - - - - - - - - - - + - - + + diff --git a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs index d8a54fcd..4bc1b2e2 100644 --- a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs +++ b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs @@ -115,12 +115,22 @@ public class FilePath : FileSystemPath, IPathObject return File.WriteAllBytesAsync(FullPath, bytes, ct); } + /// + /// Move the file to a directory. + /// + public FilePath MoveTo(FilePath destinationFile) + { + Info.MoveTo(destinationFile.FullPath, true); + // Return the new path + return destinationFile; + } + /// /// Move the file to a directory. /// public async Task MoveToAsync(DirectoryPath directory) { - await Task.Run(() => Info.MoveTo(directory.FullPath)); + await Task.Run(() => Info.MoveTo(directory.FullPath)).ConfigureAwait(false); // Return the new path return directory.JoinFile(this); } @@ -130,7 +140,7 @@ public class FilePath : FileSystemPath, IPathObject /// public async Task MoveToAsync(FilePath destinationFile) { - await Task.Run(() => Info.MoveTo(destinationFile.FullPath)); + await Task.Run(() => Info.MoveTo(destinationFile.FullPath)).ConfigureAwait(false); // Return the new path return destinationFile; } diff --git a/StabilityMatrix.Core/Models/Progress/ProgressState.cs b/StabilityMatrix.Core/Models/Progress/ProgressState.cs index 8a62751c..ec64960c 100644 --- a/StabilityMatrix.Core/Models/Progress/ProgressState.cs +++ b/StabilityMatrix.Core/Models/Progress/ProgressState.cs @@ -5,5 +5,6 @@ public enum ProgressState Inactive, Working, Success, - Failed + Failed, + Cancelled } diff --git a/StabilityMatrix.Core/Models/TrackedDownload.cs b/StabilityMatrix.Core/Models/TrackedDownload.cs new file mode 100644 index 00000000..ed62471b --- /dev/null +++ b/StabilityMatrix.Core/Models/TrackedDownload.cs @@ -0,0 +1,245 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using AsyncAwaitBestPractices; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Core.Models; + +public class TrackedDownloadProgressEventArgs : EventArgs +{ + public ProgressReport Progress { get; init; } + public ProgressState State { get; init; } +} + +public class TrackedDownload +{ + [JsonIgnore] + private IDownloadService? downloadService; + + [JsonIgnore] + private Task? downloadTask; + + [JsonIgnore] + private CancellationTokenSource? downloadCancellationTokenSource; + + [JsonIgnore] + private CancellationTokenSource? downloadPauseTokenSource; + + private CancellationTokenSource AggregateCancellationTokenSource => + CancellationTokenSource.CreateLinkedTokenSource( + downloadCancellationTokenSource?.Token ?? CancellationToken.None, + downloadPauseTokenSource?.Token ?? CancellationToken.None); + + public required Guid Id { get; init; } + + public required Uri SourceUrl { get; init; } + + public Uri? RedirectedUrl { get; init; } + + public required DirectoryPath DownloadDirectory { get; init; } + + public required string FileName { get; init; } + + public required string TempFileName { get; init; } + + public string? ExpectedHashSha256 { get; init; } + + public bool ValidateHash { get; init; } + + public ProgressState ProgressState { get; private set; } = ProgressState.Inactive; + + public Exception? Exception { get; private set; } + + #region Events + private WeakEventManager? progressUpdateEventManager; + + public event EventHandler ProgressUpdate + { + add + { + progressUpdateEventManager ??= new WeakEventManager(); + progressUpdateEventManager.AddEventHandler(value); + } + remove => progressUpdateEventManager?.RemoveEventHandler(value); + } + + protected void OnProgressUpdate(ProgressReport e) + { + progressUpdateEventManager?.RaiseEvent(this, e, nameof(ProgressUpdate)); + } + + private WeakEventManager? progressStateChangedEventManager; + + public event EventHandler ProgressStateChanged + { + add + { + progressStateChangedEventManager ??= new WeakEventManager(); + progressStateChangedEventManager.AddEventHandler(value); + } + remove => progressStateChangedEventManager?.RemoveEventHandler(value); + } + + protected void OnProgressStateChanged(ProgressState e) + { + progressStateChangedEventManager?.RaiseEvent(this, e, nameof(ProgressStateChanged)); + } + #endregion + + [MemberNotNull(nameof(downloadService))] + private void EnsureDownloadService() + { + if (downloadService == null) + { + throw new InvalidOperationException("Download service is not set."); + } + } + + private async Task StartDownloadTask(long resumeFromByte, CancellationToken cancellationToken) + { + var progress = new Progress(OnProgressUpdate); + + await downloadService!.ResumeDownloadToFileAsync( + SourceUrl.ToString(), + DownloadDirectory.JoinFile(TempFileName), + resumeFromByte, + progress, + cancellationToken: cancellationToken).ConfigureAwait(false); + + // If hash validation is enabled, validate the hash + if (ValidateHash) + { + var hash = await FileHash.GetSha256Async(DownloadDirectory.JoinFile(TempFileName), progress).ConfigureAwait(false); + if (hash != ExpectedHashSha256) + { + throw new Exception($"Hash validation for {FileName} failed, expected {ExpectedHashSha256} but got {hash}"); + } + } + } + + public void Start() + { + if (ProgressState != ProgressState.Inactive) + { + throw new InvalidOperationException($"Download state must be inactive to start, not {ProgressState}"); + } + + EnsureDownloadService(); + + downloadCancellationTokenSource = new CancellationTokenSource(); + downloadPauseTokenSource = new CancellationTokenSource(); + + downloadTask = StartDownloadTask(0, AggregateCancellationTokenSource.Token) + .ContinueWith(OnDownloadTaskCompleted); + } + + public void Resume() + { + if (ProgressState != ProgressState.Inactive) return; + + EnsureDownloadService(); + + downloadCancellationTokenSource = new CancellationTokenSource(); + downloadPauseTokenSource = new CancellationTokenSource(); + + downloadTask = StartDownloadTask(0, AggregateCancellationTokenSource.Token) + .ContinueWith(OnDownloadTaskCompleted); + } + + public void Pause() + { + if (ProgressState != ProgressState.Working) return; + + downloadPauseTokenSource?.Cancel(); + } + + public void Cancel() + { + if (ProgressState is not (ProgressState.Working or ProgressState.Inactive)) return; + + downloadCancellationTokenSource?.Cancel(); + } + + /// + /// Invoked by the task's completion callback + /// + private void OnDownloadTaskCompleted(Task task) + { + // For cancelled, check if it was actually cancelled or paused + if (task.IsCanceled) + { + // If the task was cancelled, set the state to cancelled + if (downloadCancellationTokenSource?.IsCancellationRequested == true) + { + ProgressState = ProgressState.Cancelled; + } + // If the task was not cancelled, set the state to paused + else if (downloadPauseTokenSource?.IsCancellationRequested == true) + { + ProgressState = ProgressState.Inactive; + } + else + { + throw new InvalidOperationException("Download task was cancelled but neither cancellation token was cancelled."); + } + } + // For faulted + else if (task.IsFaulted) + { + // Set the exception + Exception = task.Exception; + + // Delete the temp file + try + { + DownloadDirectory.JoinFile(TempFileName).Delete(); + } + catch (IOException) + { + } + + ProgressState = ProgressState.Failed; + } + // Otherwise success + else + { + ProgressState = ProgressState.Success; + } + + // For failed or cancelled, delete the temp file + if (ProgressState is ProgressState.Failed or ProgressState.Cancelled) + { + // Delete the temp file + try + { + DownloadDirectory.JoinFile(TempFileName).Delete(); + } + catch (IOException) + { + } + } + else if (ProgressState == ProgressState.Success) + { + // Move the temp file to the final file + DownloadDirectory.JoinFile(TempFileName).MoveTo(DownloadDirectory.JoinFile(FileName)); + } + + // For pause, just do nothing + + OnProgressStateChanged(ProgressState); + + // Dispose of the task and cancellation token + downloadTask?.Dispose(); + downloadTask = null; + downloadCancellationTokenSource?.Dispose(); + downloadCancellationTokenSource = null; + } + + public void SetDownloadService(IDownloadService service) + { + downloadService = service; + } +} From 009264f0e77f276eb4b2b114cde6af40d80b9b8d Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 21 Aug 2023 20:13:33 -0400 Subject: [PATCH 13/60] Add TrackedDownloadService --- .../Services/ITrackedDownloadService.cs | 13 ++ .../Services/TrackedDownloadService.cs | 197 ++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 StabilityMatrix.Core/Services/ITrackedDownloadService.cs create mode 100644 StabilityMatrix.Core/Services/TrackedDownloadService.cs diff --git a/StabilityMatrix.Core/Services/ITrackedDownloadService.cs b/StabilityMatrix.Core/Services/ITrackedDownloadService.cs new file mode 100644 index 00000000..ca8fcab4 --- /dev/null +++ b/StabilityMatrix.Core/Services/ITrackedDownloadService.cs @@ -0,0 +1,13 @@ +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; + +namespace StabilityMatrix.Core.Services; + +public interface ITrackedDownloadService +{ + event EventHandler? DownloadAdded; + + TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath); + + TrackedDownload NewDownload(string downloadUrl, FilePath downloadPath) => NewDownload(new Uri(downloadUrl), downloadPath); +} diff --git a/StabilityMatrix.Core/Services/TrackedDownloadService.cs b/StabilityMatrix.Core/Services/TrackedDownloadService.cs new file mode 100644 index 00000000..a6f069ce --- /dev/null +++ b/StabilityMatrix.Core/Services/TrackedDownloadService.cs @@ -0,0 +1,197 @@ +using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; + +namespace StabilityMatrix.Core.Services; + +public class TrackedDownloadService : ITrackedDownloadService, IDisposable +{ + private readonly ILogger logger; + private readonly IDownloadService downloadService; + private readonly ISettingsManager settingsManager; + + private readonly ConcurrentDictionary downloads = new(); + + /// + public event EventHandler? DownloadAdded; + + public TrackedDownloadService( + ILogger logger, + IDownloadService downloadService, + ISettingsManager settingsManager) + { + this.logger = logger; + this.downloadService = downloadService; + this.settingsManager = settingsManager; + + // Index for in-progress downloads when library dir loaded + settingsManager.RegisterOnLibraryDirSet(path => + { + var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory); + // Ignore if not exist + if (!downloadsDir.Exists) return; + + LoadInProgressDownloads(downloadsDir); + }); + } + + private void OnDownloadAdded(TrackedDownload download) + { + DownloadAdded?.Invoke(this, download); + } + + /// + /// Creates a new tracked download with backed json file and adds it to the dictionary. + /// + /// + private void AddDownload(TrackedDownload download) + { + // Set download service + download.SetDownloadService(downloadService); + + // Create json file + var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory); + var jsonFile = downloadsDir.JoinFile($"{download.Id}.json"); + var jsonFileStream = jsonFile.Info.Open(FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read); + + // Serialize to json + var json = JsonSerializer.Serialize(download); + jsonFileStream.Write(Encoding.UTF8.GetBytes(json)); + + // Add to dictionary + downloads.TryAdd(download.Id, (download, jsonFileStream)); + + // Connect to state changed event to update json file + download.ProgressStateChanged += TrackedDownload_OnProgressStateChanged; + + logger.LogDebug("Added download {Download}", download.FileName); + OnDownloadAdded(download); + } + + /// + /// Update the json file for the download. + /// + private void UpdateJsonForDownload(TrackedDownload download) + { + // Serialize to json + var json = JsonSerializer.Serialize(download); + var jsonBytes = Encoding.UTF8.GetBytes(json); + + // Write to file + var (_, fs) = downloads[download.Id]; + fs.Seek(0, SeekOrigin.Begin); + fs.Write(jsonBytes); + } + + /// + /// Handler when the download's state changes + /// + private void TrackedDownload_OnProgressStateChanged(object? sender, ProgressState e) + { + if (sender is not TrackedDownload download) + { + return; + } + + // Update json file + UpdateJsonForDownload(download); + + // If the download is completed, remove it from the dictionary and delete the json file + if (e is ProgressState.Success or ProgressState.Failed or ProgressState.Cancelled) + { + if (downloads.TryRemove(download.Id, out var downloadInfo)) + { + downloadInfo.Item2.Dispose(); + // Delete json file + new DirectoryPath(settingsManager.DownloadsDirectory).JoinFile($"{download.Id}.json").Delete(); + logger.LogDebug("Removed download {Download}", download.FileName); + } + } + } + + private void LoadInProgressDownloads(DirectoryPath downloadsDir) + { + logger.LogDebug("Indexing in-progress downloads at {DownloadsDir}...", downloadsDir); + + var jsonFiles = downloadsDir.Info.EnumerateFiles("*.json", SearchOption.TopDirectoryOnly); + + // Add to dictionary, the file name is the guid + foreach (var file in jsonFiles) + { + // Try to get a shared write handle + try + { + var fileStream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.Read); + + // Deserialize json and add to dictionary + var download = JsonSerializer.Deserialize(fileStream)!; + download.SetDownloadService(downloadService); + + downloads.TryAdd(download.Id, (download, fileStream)); + OnDownloadAdded(download); + + logger.LogDebug("Loaded in-progress download {Download}", download.FileName); + } + catch (Exception e) + { + logger.LogInformation(e, "Could not open file {File} for reading", file.Name); + } + } + } + + public TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath) + { + var download = new TrackedDownload + { + Id = Guid.NewGuid(), + SourceUrl = downloadUrl, + DownloadDirectory = downloadPath.Directory!, + FileName = downloadPath.Name, + TempFileName = NewTempFileName(downloadPath.Directory!), + }; + download.SetDownloadService(downloadService); + + AddDownload(download); + + return download; + } + + /// + /// Generate a new temp file name that is unique in the given directory. + /// In format of "Unconfirmed {id}.smdownload" + /// + /// + /// + private static string NewTempFileName(DirectoryPath parentDir) + { + FilePath? tempFile = null; + + for (var i = 0; i < 10; i++) + { + if (tempFile is {Exists: false}) + { + return tempFile.Name; + } + var id = Random.Shared.Next(1000000, 9999999); + tempFile = parentDir.JoinFile($"Unconfirmed {id}.smdownload"); + } + + throw new Exception("Failed to generate a unique temp file name."); + } + + /// + public void Dispose() + { + foreach (var (_, fs) in downloads.Values) + { + fs.Dispose(); + } + + GC.SuppressFinalize(this); + } +} From 294e80b9357b34da14cedcb8b9f674230769400e Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 21 Aug 2023 20:13:53 -0400 Subject: [PATCH 14/60] Add RegisterOnLibraryDirSet method for manager --- .../Services/ISettingsManager.cs | 8 +++++++ .../Services/SettingsManager.cs | 23 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Services/ISettingsManager.cs b/StabilityMatrix.Core/Services/ISettingsManager.cs index 04abd1ad..f8858f66 100644 --- a/StabilityMatrix.Core/Services/ISettingsManager.cs +++ b/StabilityMatrix.Core/Services/ISettingsManager.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Linq.Expressions; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Settings; namespace StabilityMatrix.Core.Services; @@ -12,10 +13,17 @@ public interface ISettingsManager bool IsLibraryDirSet { get; } string DatabasePath { get; } string ModelsDirectory { get; } + string DownloadsDirectory { get; } Settings Settings { get; } event EventHandler? LibraryDirChanged; event EventHandler? SettingsPropertyChanged; + /// + /// Register a handler that fires once when LibraryDir is first set. + /// Will fire instantly if it is already set. + /// + void RegisterOnLibraryDirSet(Action handler); + /// SettingsTransaction BeginTransaction(); diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index 4876057d..8bd487e2 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -49,12 +49,33 @@ public class SettingsManager : ISettingsManager public string DatabasePath => Path.Combine(LibraryDir, "StabilityMatrix.db"); private string SettingsPath => Path.Combine(LibraryDir, "settings.json"); public string ModelsDirectory => Path.Combine(LibraryDir, "Models"); - + public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads"); + public Settings Settings { get; private set; } = new(); public event EventHandler? LibraryDirChanged; public event EventHandler? SettingsPropertyChanged; + /// + public void RegisterOnLibraryDirSet(Action handler) + { + if (IsLibraryDirSet) + { + handler(LibraryDir); + return; + } + + LibraryDirChanged += Handler; + + return; + + void Handler(object? sender, string dir) + { + LibraryDirChanged -= Handler; + handler(dir); + } + } + /// public SettingsTransaction BeginTransaction() { From 7d1131ea1e9385e65a5d096bbc6ebfde84d91c67 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 21 Aug 2023 20:14:03 -0400 Subject: [PATCH 15/60] Add ResumeDownloadToFileAsync --- .../Services/DownloadService.cs | 99 +++++++++++++++++++ .../Services/IDownloadService.cs | 9 ++ 2 files changed, 108 insertions(+) diff --git a/StabilityMatrix.Core/Services/DownloadService.cs b/StabilityMatrix.Core/Services/DownloadService.cs index 28cbf6e0..0c446c23 100644 --- a/StabilityMatrix.Core/Services/DownloadService.cs +++ b/StabilityMatrix.Core/Services/DownloadService.cs @@ -101,6 +101,105 @@ public class DownloadService : IDownloadService progress?.Report(new ProgressReport(1f, message: "Download complete!")); } + /// + public async Task ResumeDownloadToFileAsync( + string downloadUrl, + string downloadPath, + long existingFileSize, + IProgress? progress = null, + string? httpClientName = null, + CancellationToken cancellationToken = default) + { + using var client = string.IsNullOrWhiteSpace(httpClientName) + ? httpClientFactory.CreateClient() + : httpClientFactory.CreateClient(httpClientName); + + client.Timeout = TimeSpan.FromMinutes(10); + client.DefaultRequestHeaders.UserAgent.Add( + new ProductInfoHeaderValue("StabilityMatrix", "2.0") + ); + + // Create file if it doesn't exist + if (!File.Exists(downloadPath)) + { + logger.LogInformation("Resume file doesn't exist, creating file {DownloadPath}", downloadPath); + File.Create(downloadPath).Close(); + } + + await using var file = new FileStream( + downloadPath, + FileMode.Append, + FileAccess.Write, + FileShare.None + ); + + long contentLength = 0; + + using var request = new HttpRequestMessage(); + request.Method = HttpMethod.Get; + request.RequestUri = new Uri(downloadUrl); + request.Headers.Range = new RangeHeaderValue(existingFileSize, null); + + HttpResponseMessage? response = null; + foreach (var delay in Backoff.DecorrelatedJitterBackoffV2( + TimeSpan.FromMilliseconds(50), + retryCount: 4 + )) + { + response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); + contentLength = response.Content.Headers.ContentLength ?? 0; + + if (contentLength > 0) + break; + + logger.LogDebug("Retrying get-headers for content-length"); + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + } + + if (response == null) + { + throw new ApplicationException("Response is null"); + } + + var isIndeterminate = contentLength == 0; + + await using var stream = await response.Content + .ReadAsStreamAsync(cancellationToken) + .ConfigureAwait(false); + var totalBytesRead = 0L; + var buffer = new byte[BufferSize]; + while (true) + { + var bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (bytesRead == 0) + break; + await file.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken) + .ConfigureAwait(false); + + totalBytesRead += bytesRead; + + if (isIndeterminate) + { + progress?.Report(new ProgressReport(-1, isIndeterminate: true)); + } + else + { + progress?.Report( + new ProgressReport( + current: Convert.ToUInt64(totalBytesRead), + total: Convert.ToUInt64(contentLength), + message: "Downloading..." + ) + ); + } + } + + await file.FlushAsync(cancellationToken).ConfigureAwait(false); + + progress?.Report(new ProgressReport(1f, message: "Download complete!")); + } + public async Task GetImageStreamFromUrl(string url) { using var client = httpClientFactory.CreateClient(); diff --git a/StabilityMatrix.Core/Services/IDownloadService.cs b/StabilityMatrix.Core/Services/IDownloadService.cs index 2f6fa6e7..41cad882 100644 --- a/StabilityMatrix.Core/Services/IDownloadService.cs +++ b/StabilityMatrix.Core/Services/IDownloadService.cs @@ -11,6 +11,15 @@ public interface IDownloadService string? httpClientName = null, CancellationToken cancellationToken = default ); + + Task ResumeDownloadToFileAsync( + string downloadUrl, + string downloadPath, + long existingFileSize, + IProgress? progress = null, + string? httpClientName = null, + CancellationToken cancellationToken = default + ); Task GetImageStreamFromUrl(string url); } From 4aa8dca4df1a65855ea40af21651f1fb9fc12093 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 21 Aug 2023 20:14:16 -0400 Subject: [PATCH 16/60] Add transparent-full button style --- .../Styles/ButtonStyles.axaml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml b/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml index ab433d39..18de2349 100644 --- a/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml +++ b/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml @@ -11,6 +11,7 @@ - - - - - - - - - - - - + - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/StabilityMatrix.Core/Models/Api/CivitModelFpType.cs b/StabilityMatrix.Core/Models/Api/CivitModelFpType.cs index f1250c3f..817e2ea2 100644 --- a/StabilityMatrix.Core/Models/Api/CivitModelFpType.cs +++ b/StabilityMatrix.Core/Models/Api/CivitModelFpType.cs @@ -8,6 +8,8 @@ namespace StabilityMatrix.Core.Models.Api; [SuppressMessage("ReSharper", "InconsistentNaming")] public enum CivitModelFpType { + bf16, fp16, - fp32 + fp32, + tf32 } diff --git a/StabilityMatrix.Core/Models/Api/CivitModelType.cs b/StabilityMatrix.Core/Models/Api/CivitModelType.cs index 944115be..0331e252 100644 --- a/StabilityMatrix.Core/Models/Api/CivitModelType.cs +++ b/StabilityMatrix.Core/Models/Api/CivitModelType.cs @@ -9,23 +9,29 @@ namespace StabilityMatrix.Core.Models.Api; [SuppressMessage("ReSharper", "InconsistentNaming")] public enum CivitModelType { - Unknown, [ConvertTo(SharedFolderType.StableDiffusion)] Checkpoint, [ConvertTo(SharedFolderType.TextualInversion)] TextualInversion, [ConvertTo(SharedFolderType.Hypernetwork)] Hypernetwork, - AestheticGradient, [ConvertTo(SharedFolderType.Lora)] LORA, [ConvertTo(SharedFolderType.ControlNet)] Controlnet, - Poses, - [ConvertTo(SharedFolderType.StableDiffusion)] - Model, [ConvertTo(SharedFolderType.LyCORIS)] LoCon, + [ConvertTo(SharedFolderType.VAE)] + VAE, + + // Unused/obsolete/unknown/meta options + AestheticGradient, + Model, + Poses, + Upscaler, + Wildcards, + Workflows, Other, All, + Unknown } diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs index 06cff8a4..bfe321b2 100644 --- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs @@ -18,6 +18,7 @@ public abstract class BasePackage public abstract string LicenseType { get; } public abstract string LicenseUrl { get; } public virtual string Disclaimer => string.Empty; + public virtual bool OfferInOneClickInstaller => true; /// /// Primary command to launch the package. 'Launch' buttons uses this. diff --git a/StabilityMatrix.Core/Models/Packages/Fooocus.cs b/StabilityMatrix.Core/Models/Packages/Fooocus.cs index 965599f6..b9324f3e 100644 --- a/StabilityMatrix.Core/Models/Packages/Fooocus.cs +++ b/StabilityMatrix.Core/Models/Packages/Fooocus.cs @@ -94,7 +94,7 @@ public class Fooocus : BaseGitPackage { OnConsoleOutput(s); - if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase)) + if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); From 2a64f847e5203e82e43217d9d436e22a836ebd1d Mon Sep 17 00:00:00 2001 From: JT Date: Tue, 22 Aug 2023 22:41:26 -0700 Subject: [PATCH 45/60] Fix tests --- StabilityMatrix.Tests/Avalonia/DesignDataTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/StabilityMatrix.Tests/Avalonia/DesignDataTests.cs b/StabilityMatrix.Tests/Avalonia/DesignDataTests.cs index d7d5dd87..c155e054 100644 --- a/StabilityMatrix.Tests/Avalonia/DesignDataTests.cs +++ b/StabilityMatrix.Tests/Avalonia/DesignDataTests.cs @@ -9,6 +9,7 @@ public class DesignDataTests [ClassInitialize] public static void ClassInitialize(TestContext context) { + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); StabilityMatrix.Avalonia.DesignData.DesignData.Initialize(); } From fcb5cd30667f2a3d87f182c54168d8be736e8385 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 23 Aug 2023 14:16:22 -0400 Subject: [PATCH 46/60] Test namespace cleaning --- StabilityMatrix.Tests/Avalonia/DesignDataTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Tests/Avalonia/DesignDataTests.cs b/StabilityMatrix.Tests/Avalonia/DesignDataTests.cs index c155e054..506dcb4d 100644 --- a/StabilityMatrix.Tests/Avalonia/DesignDataTests.cs +++ b/StabilityMatrix.Tests/Avalonia/DesignDataTests.cs @@ -10,12 +10,12 @@ public class DesignDataTests public static void ClassInitialize(TestContext context) { SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - StabilityMatrix.Avalonia.DesignData.DesignData.Initialize(); + DesignData.Initialize(); } // Return all properties public static IEnumerable DesignDataProperties => - typeof(StabilityMatrix.Avalonia.DesignData.DesignData).GetProperties() + typeof(DesignData).GetProperties() .Select(p => new object[] { p }); [TestMethod] From 49685c60853cf09814244e0f9c262a204348b82f Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 23 Aug 2023 14:16:45 -0400 Subject: [PATCH 47/60] Add GetDialog methods to ServiceManager --- .../Services/ServiceManager.cs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/StabilityMatrix.Avalonia/Services/ServiceManager.cs b/StabilityMatrix.Avalonia/Services/ServiceManager.cs index dc7387cf..b5606abe 100644 --- a/StabilityMatrix.Avalonia/Services/ServiceManager.cs +++ b/StabilityMatrix.Avalonia/Services/ServiceManager.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; using Microsoft.Extensions.DependencyInjection; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.Services; +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class ServiceManager { // Holds providers @@ -111,6 +115,48 @@ public class ServiceManager $"Service of type {typeof(TService)} is not registered for {typeof(T)}"); } + /// + /// Get a view model instance from runtime type + /// + [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] + public T Get(Type serviceType) + { + if (!serviceType.IsAssignableFrom(typeof(T))) + { + throw new ArgumentException( + $"Service type {serviceType} is not assignable from {typeof(T)}"); + } + + if (instances.TryGetValue(serviceType, out var instance)) + { + if (instance is null) + { + throw new ArgumentException( + $"Service of type {serviceType} was registered as null"); + } + return (T) instance; + } + + if (providers.TryGetValue(serviceType, out var provider)) + { + if (provider is null) + { + throw new ArgumentException( + $"Service of type {serviceType} was registered as null"); + } + var result = provider(); + if (result is null) + { + throw new ArgumentException( + $"Service provider for type {serviceType} returned null"); + } + return (T) result; + } + + throw new ArgumentException( + $"Service of type {serviceType} is not registered for {typeof(T)}"); + } + /// /// Get a view model instance with an initializer parameter /// @@ -129,4 +175,48 @@ public class ServiceManager initializer(instance); return instance; } + + /// + /// Get a view model instance, set as DataContext of its View, and return + /// a BetterContentDialog with that View as its Content + /// + public BetterContentDialog GetDialog() where TService : T + { + var instance = Get()!; + + if (Attribute.GetCustomAttribute(instance.GetType(), typeof(ViewAttribute)) is not ViewAttribute + viewAttr) + { + throw new InvalidOperationException($"View not found for {instance.GetType().FullName}"); + } + + if (Activator.CreateInstance(viewAttr.GetViewType()) is not Control view) + { + throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}"); + } + + return new BetterContentDialog { Content = view }; + } + + /// + /// Get a view model instance with initializer, set as DataContext of its View, and return + /// a BetterContentDialog with that View as its Content + /// + public BetterContentDialog GetDialog(Action initializer) where TService : T + { + var instance = Get(initializer)!; + + if (Attribute.GetCustomAttribute(instance.GetType(), typeof(ViewAttribute)) is not ViewAttribute + viewAttr) + { + throw new InvalidOperationException($"View not found for {instance.GetType().FullName}"); + } + + if (Activator.CreateInstance(viewAttr.GetViewType()) is not Control view) + { + throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}"); + } + + return new BetterContentDialog { Content = view }; + } } From 40a3a38864f52a3eef95ae94af14c87b8703d258 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 23 Aug 2023 17:39:53 -0400 Subject: [PATCH 48/60] Move deletion methods to DirectoryPath extension --- .../Extensions/DirectoryPathExtensions.cs | 91 ++++++++++++++ .../PackageManager/PackageCardViewModel.cs | 119 ++++-------------- 2 files changed, 113 insertions(+), 97 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs diff --git a/StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs b/StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs new file mode 100644 index 00000000..2b361970 --- /dev/null +++ b/StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Polly; +using StabilityMatrix.Core.Models.FileInterfaces; + +namespace StabilityMatrix.Avalonia.Extensions; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public static class DirectoryPathExtensions +{ + /// + /// Deletes a directory and all of its contents recursively. + /// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff. + /// + public static Task DeleteVerboseAsync(this DirectoryPath directory, ILogger? logger = default) + { + var policy = Policy.Handle() + .WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)), + onRetry: (exception, calculatedWaitDuration) => + { + logger?.LogWarning( + exception, + "Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}", + directory, calculatedWaitDuration); + }); + + return policy.ExecuteAsync(async () => + { + await Task.Run(() => { DeleteVerbose(directory, logger); }); + }); + } + + /// + /// Deletes a directory and all of its contents recursively. + /// Removes link targets without deleting the source. + /// + public static void DeleteVerbose(this DirectoryPath directory, ILogger? logger = default) + { + // Skip if directory does not exist + if (!directory.Exists) + { + return; + } + // For junction points, delete with recursive false + if (directory.IsSymbolicLink) + { + logger?.LogInformation("Removing junction point {TargetDirectory}", directory); + try + { + directory.Delete(false); + return; + } + catch (IOException ex) + { + throw new IOException($"Failed to delete junction point {directory}", ex); + } + } + // Recursively delete all subdirectories + foreach (var subDir in directory.Info.EnumerateDirectories()) + { + DeleteVerbose(subDir, logger); + } + + // Delete all files in the directory + foreach (var filePath in directory.Info.EnumerateFiles()) + { + try + { + filePath.Attributes |= FileAttributes.Normal; + filePath.Delete(); + } + catch (IOException ex) + { + throw new IOException($"Failed to delete file {filePath.FullName}", ex); + } + } + + // Delete this directory + try + { + directory.Delete(false); + } + catch (IOException ex) + { + throw new IOException($"Failed to delete directory {directory}", ex); + } + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs index 5c154c99..35f030af 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs @@ -1,20 +1,18 @@ using System; -using System.IO; -using System.Linq; using System.Threading.Tasks; using Avalonia.Controls.Notifications; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; -using NLog; -using Polly; +using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Animations; +using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; @@ -23,23 +21,25 @@ namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; public partial class PackageCardViewModel : ProgressViewModel { + private readonly ILogger logger; private readonly IPackageFactory packageFactory; private readonly INotificationService notificationService; private readonly ISettingsManager settingsManager; private readonly INavigationService navigationService; - private readonly Logger logger = LogManager.GetCurrentClassLogger(); [ObservableProperty] private InstalledPackage? package; - [ObservableProperty] private Uri cardImage; + [ObservableProperty] private Uri? cardImage; [ObservableProperty] private bool isUpdateAvailable; - [ObservableProperty] private string installedVersion; + [ObservableProperty] private string? installedVersion; public PackageCardViewModel( + ILogger logger, IPackageFactory packageFactory, INotificationService notificationService, ISettingsManager settingsManager, INavigationService navigationService) { + this.logger = logger; this.packageFactory = packageFactory; this.notificationService = notificationService; this.settingsManager = settingsManager; @@ -94,9 +94,10 @@ public partial class PackageCardViewModel : ProgressViewModel Text = "Uninstalling..."; IsIndeterminate = true; Value = -1; - - var deleteTask = DeleteDirectoryAsync(Path.Combine(settingsManager.LibraryDir, - Package.LibraryPath)); + + var packagePath = new DirectoryPath(settingsManager.LibraryDir, Package.LibraryPath); + var deleteTask = packagePath.DeleteVerboseAsync(logger); + var taskResult = await notificationService.TryAsync(deleteTask, "Some files could not be deleted. Please close any open files in the package directory and try again."); if (taskResult.IsSuccessful) @@ -122,20 +123,22 @@ public partial class PackageCardViewModel : ProgressViewModel var basePackage = packageFactory[Package.PackageName!]; if (basePackage == null) { - logger.Warn("Could not find package {SelectedPackagePackageName}", + logger.LogWarning("Could not find package {SelectedPackagePackageName}", Package.PackageName); notificationService.Show("Invalid Package type", $"Package {Package.PackageName.ToRepr()} is not a valid package type", NotificationType.Error); return; } + + var packageName = Package.DisplayName ?? Package.PackageName ?? ""; - Text = $"Updating {Package.DisplayName}"; + Text = $"Updating {packageName}"; IsIndeterminate = true; var progressId = Guid.NewGuid(); EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, - Package.DisplayName, + Package.DisplayName ?? Package.PackageName!, new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update))); try @@ -152,7 +155,7 @@ public partial class PackageCardViewModel : ProgressViewModel EventManager.Instance.OnGlobalProgressChanged(percent); EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, - Package.DisplayName, progress)); + packageName, progress)); }); var updateResult = await basePackage.Update(Package, progress); @@ -170,15 +173,15 @@ public partial class PackageCardViewModel : ProgressViewModel InstalledVersion = Package.DisplayVersion ?? "Unknown"; EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, - Package.DisplayName, + packageName, new ProgressReport(1f, "Update complete", type: ProgressType.Update))); } catch (Exception e) { - logger.Error(e, "Error Updating Package ({PackageName})", basePackage.Name); + logger.LogError(e, "Error Updating Package ({PackageName})", basePackage.Name); notificationService.ShowPersistent($"Error Updating {Package.DisplayName}", e.Message, NotificationType.Error); EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, - Package.DisplayName, + packageName, new ProgressReport(0f, "Update failed", type: ProgressType.Update), Failed: true)); } finally @@ -224,86 +227,8 @@ public partial class PackageCardViewModel : ProgressViewModel } catch (Exception e) { - logger.Error(e, $"Error checking {Package.PackageName} for updates"); + logger.LogError(e, "Error checking {PackageName} for updates", Package.PackageName); return false; } } - - /// - /// Deletes a directory and all of its contents recursively. - /// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff. - /// - /// - private Task DeleteDirectoryAsync(string targetDirectory) - { - var policy = Policy.Handle() - .WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)), - onRetry: (exception, calculatedWaitDuration) => - { - logger.Warn( - exception, - "Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}", - targetDirectory, calculatedWaitDuration); - }); - - return policy.ExecuteAsync(async () => - { - await Task.Run(() => - { - DeleteDirectory(targetDirectory); - }); - }); - } - - private void DeleteDirectory(string targetDirectory) - { - // Skip if directory does not exist - if (!Directory.Exists(targetDirectory)) - { - return; - } - // For junction points, delete with recursive false - if (new DirectoryInfo(targetDirectory).LinkTarget != null) - { - logger.Info("Removing junction point {TargetDirectory}", targetDirectory); - try - { - Directory.Delete(targetDirectory, false); - return; - } - catch (IOException ex) - { - throw new IOException($"Failed to delete junction point {targetDirectory}", ex); - } - } - // Recursively delete all subdirectories - var subdirectoryEntries = Directory.GetDirectories(targetDirectory); - foreach (var subdirectoryPath in subdirectoryEntries) - { - DeleteDirectory(subdirectoryPath); - } - // Delete all files in the directory - var fileEntries = Directory.GetFiles(targetDirectory); - foreach (var filePath in fileEntries) - { - try - { - File.SetAttributes(filePath, FileAttributes.Normal); - File.Delete(filePath); - } - catch (IOException ex) - { - throw new IOException($"Failed to delete file {filePath}", ex); - } - } - // Delete the target directory itself - try - { - Directory.Delete(targetDirectory, false); - } - catch (IOException ex) - { - throw new IOException($"Failed to delete directory {targetDirectory}", ex); - } - } } From 723a95f4aabb6e5d3c01635e4e6cd0f666b52607 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 23 Aug 2023 17:40:11 -0400 Subject: [PATCH 49/60] Add Equality comparer for InstalledPackage --- .../Helper/PropertyComparer.cs | 24 +++++++++++++++++++ .../Models/InstalledPackage.cs | 5 +++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 StabilityMatrix.Core/Helper/PropertyComparer.cs diff --git a/StabilityMatrix.Core/Helper/PropertyComparer.cs b/StabilityMatrix.Core/Helper/PropertyComparer.cs new file mode 100644 index 00000000..f00f9665 --- /dev/null +++ b/StabilityMatrix.Core/Helper/PropertyComparer.cs @@ -0,0 +1,24 @@ +namespace StabilityMatrix.Core.Helper; + +public class PropertyComparer : IEqualityComparer where T : class +{ + private Func Expr { get; set; } + + public PropertyComparer(Func expr) + { + Expr = expr; + } + public bool Equals(T? x, T? y) + { + if (x == null || y == null) return false; + + var first = Expr.Invoke(x); + var second = Expr.Invoke(y); + + return first.Equals(second); + } + public int GetHashCode(T obj) + { + return obj.GetHashCode(); + } +} diff --git a/StabilityMatrix.Core/Models/InstalledPackage.cs b/StabilityMatrix.Core/Models/InstalledPackage.cs index 627582a1..26d00423 100644 --- a/StabilityMatrix.Core/Models/InstalledPackage.cs +++ b/StabilityMatrix.Core/Models/InstalledPackage.cs @@ -39,7 +39,7 @@ public class InstalledPackage public DateTimeOffset? LastUpdateCheck { get; set; } public bool UpdateAvailable { get; set; } - + /// /// Get the path as a relative sub-path of the relative path. /// If not a sub-path, return null. @@ -158,6 +158,9 @@ public class InstalledPackage LibraryPath = System.IO.Path.Combine("Packages", packageFolderName); } + public static IEqualityComparer Comparer { get; } = + new PropertyComparer(p => p.Id); + protected bool Equals(InstalledPackage other) { return Id.Equals(other.Id); From 6fbd07edee03a1c3ff669b1aca2c70bf6516853d Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 23 Aug 2023 19:55:50 -0400 Subject: [PATCH 50/60] Add importing for unknown packages found in index --- StabilityMatrix.Avalonia/App.axaml.cs | 15 +- .../DesignData/DesignData.cs | 21 +- .../Extensions/DirectoryPathExtensions.cs | 2 +- .../Languages/Resources.Designer.cs | 63 +++++ .../Languages/Resources.resx | 21 ++ .../Services/ServiceManager.cs | 2 + .../StabilityMatrix.Avalonia.csproj | 1 + .../Dialogs/PackageImportViewModel.cs | 221 ++++++++++++++++++ .../PackageManager/PackageCardViewModel.cs | 70 +++++- .../ViewModels/PackageManagerViewModel.cs | 118 +++++++--- .../Views/Dialogs/PackageImportDialog.axaml | 51 ++++ .../Dialogs/PackageImportDialog.axaml.cs | 17 ++ .../Views/PackageManagerPage.axaml | 55 ++++- .../Models/Packages/BasePackage.cs | 4 +- .../Models/Packages/UnknownPackage.cs | 105 +++++++++ .../Models/UnknownInstalledPackage.cs | 17 ++ 16 files changed, 721 insertions(+), 62 deletions(-) create mode 100644 StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml create mode 100644 StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs create mode 100644 StabilityMatrix.Core/Models/Packages/UnknownPackage.cs create mode 100644 StabilityMatrix.Core/Models/UnknownInstalledPackage.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 5fe48863..78baebba 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -247,14 +247,16 @@ public sealed class App : Application services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + + // Dialog view models (singleton) services.AddSingleton(); services.AddSingleton(); // Other transients (usually sub view models) - services.AddTransient() - .AddTransient() - .AddTransient(); - + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); // Global progress @@ -280,7 +282,9 @@ public sealed class App : Application .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) - .Register(provider.GetRequiredService)); + .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) + ); } internal static void ConfigureViews(IServiceCollection services) @@ -300,6 +304,7 @@ public sealed class App : Application services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Controls services.AddTransient(); diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 2aa2ab9f..69b8d7f0 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -4,7 +4,6 @@ using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia.Models; @@ -12,8 +11,8 @@ using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; +using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.Dialogs; -using StabilityMatrix.Avalonia.ViewModels.PackageManager; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Helper; @@ -25,8 +24,6 @@ using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; -using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile; -using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder; namespace StabilityMatrix.Avalonia.DesignData; @@ -296,10 +293,15 @@ public static class DesignData { var settings = Services.GetRequiredService(); var vm = Services.GetRequiredService(); - vm.Packages = new ObservableCollection( - settings.Settings.InstalledPackages.Select(p => - DialogFactory.Get(viewModel => viewModel.Package = p))); - vm.Packages.First().IsUpdateAvailable = true; + + vm.SetPackages(settings.Settings.InstalledPackages); + vm.SetUnknownPackages(new InstalledPackage[] + { + UnknownInstalledPackage.FromDirectoryName("sd-unknown"), + }); + + vm.PackageCards[0].IsUpdateAvailable = true; + return vm; } } @@ -405,6 +407,9 @@ public static class DesignData }; }); + public static PackageImportViewModel PackageImportViewModel => + DialogFactory.Get(); + public static RefreshBadgeViewModel RefreshBadgeViewModel => new() { State = ProgressState.Success diff --git a/StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs b/StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs index 2b361970..508d0818 100644 --- a/StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs @@ -69,7 +69,7 @@ public static class DirectoryPathExtensions { try { - filePath.Attributes |= FileAttributes.Normal; + filePath.Attributes = FileAttributes.Normal; filePath.Delete(); } catch (IOException ex) diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index e139dcf2..27757b99 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -68,6 +68,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Import. + /// + public static string Action_Import { + get { + return ResourceManager.GetString("Action_Import", resourceCulture); + } + } + /// /// Looks up a localized string similar to Launch. /// @@ -113,6 +122,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Branches. + /// + public static string Label_Branches { + get { + return ResourceManager.GetString("Label_Branches", resourceCulture); + } + } + /// /// Looks up a localized string similar to Language. /// @@ -122,6 +140,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Package Type. + /// + public static string Label_PackageType { + get { + return ResourceManager.GetString("Label_PackageType", resourceCulture); + } + } + /// /// Looks up a localized string similar to Relaunch Required. /// @@ -131,6 +158,42 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Releases. + /// + public static string Label_Releases { + get { + return ResourceManager.GetString("Label_Releases", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown Package. + /// + public static string Label_UnknownPackage { + get { + return ResourceManager.GetString("Label_UnknownPackage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Version. + /// + public static string Label_Version { + get { + return ResourceManager.GetString("Label_Version", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Version Type. + /// + public static string Label_VersionType { + get { + return ResourceManager.GetString("Label_VersionType", resourceCulture); + } + } + /// /// Looks up a localized string similar to Relaunch is required for new language option to take effect. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index fa598c67..93626903 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -45,4 +45,25 @@ Relaunch Required + + Unknown Package + + + Import + + + Package Type + + + Version + + + Version Type + + + Releases + + + Branches + diff --git a/StabilityMatrix.Avalonia/Services/ServiceManager.cs b/StabilityMatrix.Avalonia/Services/ServiceManager.cs index b5606abe..ee957cec 100644 --- a/StabilityMatrix.Avalonia/Services/ServiceManager.cs +++ b/StabilityMatrix.Avalonia/Services/ServiceManager.cs @@ -217,6 +217,8 @@ public class ServiceManager throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}"); } + view.DataContext = instance; + return new BetterContentDialog { Content = view }; } } diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index ca4fc938..bad8f426 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -25,6 +25,7 @@ + diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs new file mode 100644 index 00000000..40ed6a88 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; +using Avalonia.Controls; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using NLog; +using StabilityMatrix.Avalonia.Views.Dialogs; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper.Factory; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Packages; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; + +[View(typeof(PackageImportDialog))] +public partial class PackageImportViewModel : ContentDialogViewModelBase +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly IPackageFactory packageFactory; + private readonly ISettingsManager settingsManager; + + [ObservableProperty] private DirectoryPath? packagePath; + [ObservableProperty] private BasePackage? selectedBasePackage; + + public IReadOnlyList AvailablePackages + => packageFactory.GetAllAvailablePackages().ToImmutableArray(); + + [ObservableProperty] private PackageVersion? selectedVersion; + + [ObservableProperty] private ObservableCollection? availableCommits; + [ObservableProperty] private ObservableCollection? availableVersions; + + [ObservableProperty] private GitCommit? selectedCommit; + + // Version types (release or commit) + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ReleaseLabelText), + nameof(IsReleaseMode), nameof(SelectedVersion))] + private PackageVersionType selectedVersionType = PackageVersionType.Commit; + + [ObservableProperty] + [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 PackageImportViewModel( + IPackageFactory packageFactory, + ISettingsManager settingsManager) + { + this.packageFactory = packageFactory; + this.settingsManager = settingsManager; + } + + public override async Task OnLoadedAsync() + { + SelectedBasePackage ??= AvailablePackages[0]; + + if (Design.IsDesignMode) return; + // Populate available versions + try + { + if (IsReleaseMode) + { + var versions = (await SelectedBasePackage.GetAllVersions()).ToList(); + AvailableVersions = new ObservableCollection(versions); + 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 + })); + UpdateSelectedVersionToLatestMain(); + } + } + catch (Exception e) + { + Logger.Warn("Error getting versions: {Exception}", e.ToString()); + } + } + + private static string GetDisplayVersion(string version, string? branch) + { + return branch == null ? version : $"{branch}@{version[..7]}"; + } + + // When available version types change, reset selected version type if not compatible + partial void OnAvailableVersionTypesChanged(PackageVersionType value) + { + if (!value.HasFlag(SelectedVersionType)) + { + SelectedVersionType = value; + } + } + + // When changing branch / release modes, refresh + // ReSharper disable once UnusedParameterInPartialMethod + partial void OnSelectedVersionTypeChanged(PackageVersionType value) + => OnSelectedBasePackageChanged(SelectedBasePackage); + + partial void OnSelectedBasePackageChanged(BasePackage? value) + { + if (value is null || SelectedBasePackage is null) + { + AvailableVersions?.Clear(); + AvailableCommits?.Clear(); + return; + } + + AvailableVersions?.Clear(); + AvailableCommits?.Clear(); + + AvailableVersionTypes = SelectedBasePackage.ShouldIgnoreReleases + ? PackageVersionType.Commit + : PackageVersionType.GithubRelease | PackageVersionType.Commit; + + if (Design.IsDesignMode) return; + + Dispatcher.UIThread.InvokeAsync(async () => + { + Logger.Debug($"Release mode: {IsReleaseMode}"); + var versions = (await value.GetAllVersions(IsReleaseMode)).ToList(); + + if (!versions.Any()) return; + + AvailableVersions = new ObservableCollection(versions); + Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}"); + SelectedVersion = AvailableVersions[0]; + + if (!IsReleaseMode) + { + var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList(); + if (commits is null || commits.Count == 0) return; + + AvailableCommits = new ObservableCollection(commits); + SelectedCommit = AvailableCommits[0]; + UpdateSelectedVersionToLatestMain(); + } + }).SafeFireAndForget(); + } + + private void UpdateSelectedVersionToLatestMain() + { + if (AvailableVersions is null) + { + SelectedVersion = null; + } + else + { + // First try to find master + var version = AvailableVersions.FirstOrDefault(x => x.TagName == "master"); + // If not found, try main + version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main"); + + // If still not found, just use the first one + version ??= AvailableVersions[0]; + + SelectedVersion = version; + } + } + + public void AddPackageWithCurrentInputs() + { + if (SelectedBasePackage is null || PackagePath is null) + return; + + string version; + if (IsReleaseMode) + { + version = SelectedVersion?.TagName ?? + throw new NullReferenceException("Selected version is null"); + } + else + { + version = 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, + LaunchCommand = SelectedBasePackage.LaunchCommand, + LastUpdateCheck = DateTimeOffset.Now, + }; + + settingsManager.Transaction(s => s.InstalledPackages.Add(package)); + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs index 35f030af..d8b538e9 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs @@ -1,18 +1,22 @@ using System; using System.Threading.Tasks; +using Avalonia.Controls; using Avalonia.Controls.Notifications; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; @@ -26,24 +30,28 @@ public partial class PackageCardViewModel : ProgressViewModel private readonly INotificationService notificationService; private readonly ISettingsManager settingsManager; private readonly INavigationService navigationService; + private readonly ServiceManager vmFactory; [ObservableProperty] private InstalledPackage? package; - [ObservableProperty] private Uri? cardImage; + [ObservableProperty] private string? cardImageSource; [ObservableProperty] private bool isUpdateAvailable; [ObservableProperty] private string? installedVersion; - + [ObservableProperty] private bool isUnknownPackage; + public PackageCardViewModel( ILogger logger, IPackageFactory packageFactory, INotificationService notificationService, ISettingsManager settingsManager, - INavigationService navigationService) + INavigationService navigationService, + ServiceManager vmFactory) { this.logger = logger; this.packageFactory = packageFactory; this.notificationService = notificationService; this.settingsManager = settingsManager; this.navigationService = navigationService; + this.vmFactory = vmFactory; } partial void OnPackageChanged(InstalledPackage? value) @@ -51,9 +59,21 @@ public partial class PackageCardViewModel : ProgressViewModel if (string.IsNullOrWhiteSpace(value?.PackageName)) return; - var basePackage = packageFactory[value.PackageName]; - CardImage = basePackage?.PreviewImageUri ?? Assets.NoImage; - InstalledVersion = value.DisplayVersion ?? "Unknown"; + if (value.PackageName == UnknownPackage.Key) + { + IsUnknownPackage = true; + CardImageSource = ""; + InstalledVersion = "Unknown"; + } + else + { + IsUnknownPackage = false; + + var basePackage = packageFactory[value.PackageName]; + CardImageSource = basePackage?.PreviewImageUri.ToString() + ?? Assets.NoImage.ToString(); + InstalledVersion = value.DisplayVersion ?? "Unknown"; + } } public override async Task OnLoadedAsync() @@ -105,11 +125,14 @@ public partial class PackageCardViewModel : ProgressViewModel notificationService.Show(new Notification("Success", $"Package {Package.DisplayName} uninstalled", NotificationType.Success)); - - settingsManager.Transaction(settings => + + if (!IsUnknownPackage) { - settings.RemoveInstalledPackageAndUpdateActive(Package); - }); + settingsManager.Transaction(settings => + { + settings.RemoveInstalledPackageAndUpdateActive(Package); + }); + } EventManager.Instance.OnInstalledPackagesChanged(); } @@ -118,7 +141,7 @@ public partial class PackageCardViewModel : ProgressViewModel public async Task Update() { - if (Package == null) return; + if (Package is null || IsUnknownPackage) return; var basePackage = packageFactory[Package.PackageName!]; if (basePackage == null) @@ -191,6 +214,29 @@ public partial class PackageCardViewModel : ProgressViewModel Text = ""; } } + + public async Task Import() + { + if (!IsUnknownPackage || Design.IsDesignMode) return; + + PackageImportViewModel viewModel = null!; + var dialog = vmFactory.GetDialog(vm => + { + vm.PackagePath = new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException()); + viewModel = vm; + }); + + dialog.PrimaryButtonText = Resources.Action_Import; + dialog.CloseButtonText = Resources.Action_Cancel; + dialog.DefaultButton = ContentDialogButton.Primary; + + var result = await dialog.ShowAsync(); + if (result == ContentDialogResult.Primary) + { + viewModel.AddPackageWithCurrentInputs(); + EventManager.Instance.OnInstalledPackagesChanged(); + } + } public async Task OpenFolder() { @@ -202,7 +248,7 @@ public partial class PackageCardViewModel : ProgressViewModel private async Task HasUpdate() { - if (Package == null) + if (Package == null || IsUnknownPackage || Design.IsDesignMode) return false; var basePackage = packageFactory[Package.PackageName!]; diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs index 6994b721..ac98e1b6 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs @@ -1,11 +1,13 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; -using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; -using CommunityToolkit.Mvvm.ComponentModel; +using DynamicData; +using DynamicData.Binding; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; @@ -17,6 +19,8 @@ using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; @@ -34,39 +38,73 @@ public partial class PackageManagerViewModel : PageViewModelBase private readonly IPackageFactory packageFactory; private readonly ServiceManager dialogFactory; - public PackageManagerViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory, - ServiceManager dialogFactory) + public override string Title => "Packages"; + public override IconSource IconSource => + new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true }; + + /// + /// List of installed packages + /// + private readonly SourceCache installedPackages = new(p => p.Id); + + /// + /// List of indexed packages without a corresponding installed package + /// + private readonly SourceCache unknownInstalledPackages = new(p => p.Id); + + public IObservableCollection Packages { get; } = + new ObservableCollectionExtended(); + + public IObservableCollection PackageCards { get; } = + new ObservableCollectionExtended(); + + public PackageManagerViewModel( + ISettingsManager settingsManager, + IPackageFactory packageFactory, + ServiceManager dialogFactory + ) { this.settingsManager = settingsManager; this.packageFactory = packageFactory; this.dialogFactory = dialogFactory; - + EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged; + + var installed = installedPackages.Connect(); + var unknown = unknownInstalledPackages.Connect(); + + installed + .Or(unknown) + .DeferUntilLoaded() + .Bind(Packages) + .Transform(p => dialogFactory.Get(vm => + { + vm.Package = p; + vm.OnLoadedAsync().SafeFireAndForget(); + })) + .Bind(PackageCards) + .Subscribe(); } - [ObservableProperty] private ObservableCollection packages; + public void SetPackages(IEnumerable packages) + { + installedPackages.Edit(s => s.Load(packages)); + } + + public void SetUnknownPackages(IEnumerable packages) + { + unknownInstalledPackages.Edit(s => s.Load(packages)); + } - public override bool CanNavigateNext { get; protected set; } = true; - public override bool CanNavigatePrevious { get; protected set; } - public override string Title => "Packages"; - public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true}; - public override async Task OnLoadedAsync() { - if (Design.IsDesignMode) return; - - var installedPackages = settingsManager.Settings.InstalledPackages; - Packages = new ObservableCollection(installedPackages.Select( - package => dialogFactory.Get(vm => - { - vm.Package = package; - return vm; - }))); + if (Design.IsDesignMode) + return; - foreach (var package in Packages) - { - await package.OnLoadedAsync(); - } + installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer); + + var currentUnknown = await Task.Run(IndexUnknownPackages); + unknownInstalledPackages.Edit(s => s.Load(currentUnknown)); } public async Task ShowInstallDialog() @@ -83,16 +121,40 @@ public partial class PackageManagerViewModel : PageViewModelBase IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false, - Content = new InstallerDialog - { - DataContext = viewModel - } + Content = new InstallerDialog { DataContext = viewModel } }; await dialog.ShowAsync(); await OnLoadedAsync(); } + private IEnumerable IndexUnknownPackages() + { + var packageDir = new DirectoryPath(settingsManager.LibraryDir).JoinDir("Packages"); + + if (!packageDir.Exists) + { + yield break; + } + + var currentPackages = settingsManager.Settings.InstalledPackages.ToImmutableArray(); + + foreach (var subDir in packageDir.Info + .EnumerateDirectories() + .Select(info => new DirectoryPath(info))) + { + var expectedLibraryPath = $"Packages{Path.DirectorySeparatorChar}{subDir.Name}"; + + // Skip if the package is already installed + if (currentPackages.Any(p => p.LibraryPath == expectedLibraryPath)) + { + continue; + } + + yield return UnknownInstalledPackage.FromDirectoryName(subDir.Name); + } + } + private void OnInstalledPackagesChanged(object? sender, EventArgs e) => OnLoadedAsync().SafeFireAndForget(); } diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml new file mode 100644 index 00000000..8cdbf093 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs new file mode 100644 index 00000000..a5f8cf50 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Markup.Xaml; +using StabilityMatrix.Avalonia.Controls; + +namespace StabilityMatrix.Avalonia.Views.Dialogs; + +public partial class PackageImportDialog : UserControlBase +{ + public PackageImportDialog() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml b/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml index 74242ac3..72f22091 100644 --- a/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml +++ b/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml @@ -4,11 +4,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:packageManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.PackageManager" xmlns:faicon="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" + xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:DataType="viewModels:PackageManagerViewModel" x:CompileBindings="True" @@ -24,7 +24,7 @@ + ItemsSource="{Binding PackageCards}"> @@ -36,7 +36,7 @@ + + + diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs index bfe321b2..34b9acff 100644 --- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs @@ -73,8 +73,8 @@ public abstract class BasePackage public abstract Task> GetAllBranches(); public abstract Task> GetAllReleases(); - public abstract string DownloadLocation { get; } - public abstract string InstallLocation { get; set; } + public virtual string? DownloadLocation { get; } + public virtual string? InstallLocation { get; set; } public event EventHandler? ConsoleOutput; public event EventHandler? Exited; diff --git a/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs b/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs new file mode 100644 index 00000000..069d1368 --- /dev/null +++ b/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs @@ -0,0 +1,105 @@ +using Octokit; +using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; + +namespace StabilityMatrix.Core.Models.Packages; + +public class UnknownPackage : BasePackage +{ + public static string Key => "unknown-package"; + public override string Name => Key; + public override string DisplayName { get; set; } = "Unknown Package"; + public override string Author => ""; + + public override string GithubUrl => ""; + public override string LicenseType => "AGPL-3.0"; + public override string LicenseUrl => + "https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE"; + public override string Blurb => "A dank interface for diffusion"; + public override string LaunchCommand => "test"; + + public override Uri PreviewImageUri => new(""); + + public override IReadOnlyList ExtraLaunchCommands => new[] + { + "test-config", + }; + + /// + public override Task DownloadPackage(string version, bool isCommitHash, string? branch, IProgress? progress = null) + { + throw new NotImplementedException(); + } + + /// + public override Task InstallPackage(IProgress? progress = null) + { + throw new NotImplementedException(); + } + + public override Task RunPackage(string installedPackagePath, string command, string arguments) + { + throw new NotImplementedException(); + } + + /// + public override Task SetupModelFolders(DirectoryPath installDirectory) + { + throw new NotImplementedException(); + } + + /// + public override Task UpdateModelFolders(DirectoryPath installDirectory) + { + throw new NotImplementedException(); + } + + /// + public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) + { + throw new NotImplementedException(); + } + + /// + public override void Shutdown() + { + throw new NotImplementedException(); + } + + /// + public override Task WaitForShutdown() + { + throw new NotImplementedException(); + } + + /// + public override Task CheckForUpdates(InstalledPackage package) + { + throw new NotImplementedException(); + } + + /// + public override Task Update(InstalledPackage installedPackage, IProgress? progress = null, + bool includePrerelease = false) + { + throw new NotImplementedException(); + } + + /// + public override Task> GetReleaseTags() => Task.FromResult(Enumerable.Empty()); + + public override List LaunchOptions => new(); + public override Task GetLatestVersion() => Task.FromResult(string.Empty); + + public override Task> GetAllVersions(bool isReleaseMode = true) => Task.FromResult(Enumerable.Empty()); + + /// + public override Task?> GetAllCommits(string branch, int page = 1, int perPage = 10) => Task.FromResult?>(null); + + /// + public override Task> GetAllBranches() => Task.FromResult(Enumerable.Empty()); + + /// + public override Task> GetAllReleases() => Task.FromResult(Enumerable.Empty()); +} diff --git a/StabilityMatrix.Core/Models/UnknownInstalledPackage.cs b/StabilityMatrix.Core/Models/UnknownInstalledPackage.cs new file mode 100644 index 00000000..3d9360bc --- /dev/null +++ b/StabilityMatrix.Core/Models/UnknownInstalledPackage.cs @@ -0,0 +1,17 @@ +using StabilityMatrix.Core.Models.Packages; + +namespace StabilityMatrix.Core.Models; + +public class UnknownInstalledPackage : InstalledPackage +{ + public static UnknownInstalledPackage FromDirectoryName(string name) + { + return new UnknownInstalledPackage + { + Id = Guid.NewGuid(), + PackageName = UnknownPackage.Key, + DisplayName = name, + LibraryPath = $"Packages{System.IO.Path.DirectorySeparatorChar}{name}", + }; + } +} From bf9ed94770abde7dbd1345348df5dc362557f3f8 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 23 Aug 2023 20:15:50 -0400 Subject: [PATCH 51/60] chagenlog update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88267e83..3a9e78ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Added "Skip to First/Last Page" buttons to the Model Browser - Added VAE as a checkpoint category in the Model Browser - Pause/Resume/Cancel buttons on downloads popup. Paused downloads persists and may be resumed after restarting the app +- Unknown Package installs in the Package directory will now show up with a button to import them ### Fixed - Fixed issue where model version wouldn't be selected in the "All Versions" section of the Model Browser - Improved Checkpoints page indexing performance From be2f5f8f264e21dfb53fcc36cf2e38d86b58e1c8 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 24 Aug 2023 16:34:14 -0400 Subject: [PATCH 52/60] Fix Dictionary error when launch arguments saved with duplicate arguments --- CHANGELOG.md | 1 + StabilityMatrix.Core/Models/LaunchOptionCard.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a9e78ca..9657f189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Fixed issue where Checkpoints page may not show all checkpoints after clearing search filter - Fixed issue where Checkpoints page may show incorrect checkpoints for the given filter after changing pages - Fixed issue where Open Web UI button would try to load 0.0.0.0 addresses +- Fixed Dictionary error when launch arguments saved with duplicate arguments ### Changed - Changed update method for SD.Next to use the built-in upgrade functionality - Model Browser navigation buttons are no longer disabled while changing pages diff --git a/StabilityMatrix.Core/Models/LaunchOptionCard.cs b/StabilityMatrix.Core/Models/LaunchOptionCard.cs index 3e5dddf5..8882029c 100644 --- a/StabilityMatrix.Core/Models/LaunchOptionCard.cs +++ b/StabilityMatrix.Core/Models/LaunchOptionCard.cs @@ -45,8 +45,13 @@ public readonly record struct LaunchOptionCard // During card creation, store dict of options with initial values var initialOptions = new Dictionary(); - // Dict of - var launchArgsDict = launchArgs.ToDictionary(launchArg => launchArg.Name); + // To dictionary ignoring duplicates + var launchArgsDict = launchArgs + .ToLookup(launchArg => launchArg.Name) + .ToDictionary( + group => group.Key, + group => group.First() + ); // Create cards foreach (var definition in definitions) From 0f8691a8a4532f478f93feb740dd51ce8440e5b6 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 24 Aug 2023 16:35:05 -0400 Subject: [PATCH 53/60] Fix launch arguments search --- CHANGELOG.md | 1 + .../Controls/BetterContentDialog.cs | 21 +++++++ .../Dialogs/LaunchOptionsViewModel.cs | 61 ++++++++++++------- .../ViewModels/LaunchPageViewModel.cs | 4 +- .../Views/Dialogs/LaunchOptionsDialog.axaml | 3 +- 5 files changed, 67 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9657f189..c593229e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Fixed issue where Checkpoints page may show incorrect checkpoints for the given filter after changing pages - Fixed issue where Open Web UI button would try to load 0.0.0.0 addresses - Fixed Dictionary error when launch arguments saved with duplicate arguments +- Fixed Launch arguments search not working ### Changed - Changed update method for SD.Next to use the built-in upgrade functionality - Model Browser navigation buttons are no longer disabled while changing pages diff --git a/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs b/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs index 33511dbc..732715a3 100644 --- a/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs +++ b/StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs @@ -132,6 +132,15 @@ public class BetterContentDialog : ContentDialog get => GetValue(MaxDialogHeightProperty); set => SetValue(MaxDialogHeightProperty, value); } + + public static readonly StyledProperty ContentMarginProperty = AvaloniaProperty.Register( + "ContentMargin"); + + public Thickness ContentMargin + { + get => GetValue(ContentMarginProperty); + set => SetValue(ContentMarginProperty, value); + } public BetterContentDialog() @@ -205,6 +214,18 @@ public class BetterContentDialog : ContentDialog TryBindButtons(); } + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + var background = e.NameScope.Find("BackgroundElement"); + if (background is not null) + { + background.Margin = ContentMargin; + } + } + private void OnLoaded(object? sender, RoutedEventArgs? e) { TryBindButtons(); diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs index 519fe8c1..9979fd6f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; +using System.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FuzzySharp; +using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper.Cache; @@ -14,26 +18,28 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(LaunchOptionsDialog))] public partial class LaunchOptionsViewModel : ContentDialogViewModelBase { + private readonly ILogger logger; private readonly LRUCache> cache = new(100); - - [ObservableProperty] private string title = "Launch Options"; - [ObservableProperty] private bool isSearchBoxEnabled = true; - + + [ObservableProperty] + private string title = "Launch Options"; + + [ObservableProperty] + private bool isSearchBoxEnabled = true; + [ObservableProperty] - [NotifyPropertyChangedFor(nameof(FilteredCards))] private string searchText = string.Empty; - - [ObservableProperty] + + [ObservableProperty] private IReadOnlyList? filteredCards; - + public IReadOnlyList? Cards { get; set; } - + /// /// Return cards that match the search text /// - private IReadOnlyList? GetFilteredCards() + private IReadOnlyList? GetFilteredCards(string? text) { - var text = SearchText; if (string.IsNullOrWhiteSpace(text) || text.Length < 2) { return Cards; @@ -50,18 +56,30 @@ public partial class LaunchOptionsViewModel : ContentDialogViewModelBase Type = LaunchOptionType.Bool, Options = Array.Empty() }; - - var extracted = Process - .ExtractTop(searchCard, Cards, c => c.Title.ToLowerInvariant()); - var results = extracted - .Where(r => r.Score > 40) - .Select(r => r.Value) - .ToImmutableList(); + + var extracted = Process.ExtractTop(searchCard, Cards, c => c.Title.ToLowerInvariant()); + var results = extracted.Where(r => r.Score > 40).Select(r => r.Value).ToImmutableList(); cache.Add(text, results); return results; } - public void UpdateFilterCards() => FilteredCards = GetFilteredCards(); + public void UpdateFilterCards() => FilteredCards = GetFilteredCards(SearchText); + + public LaunchOptionsViewModel(ILogger logger) + { + this.logger = logger; + + Observable + .FromEventPattern(this, nameof(PropertyChanged)) + .Where(x => x.EventArgs.PropertyName == nameof(SearchText)) + .Throttle(TimeSpan.FromMilliseconds(50)) + .Select(_ => SearchText) + .ObserveOn(SynchronizationContext.Current!) + .Subscribe( + text => FilteredCards = GetFilteredCards(text), + err => logger.LogError(err, "Error while filtering launch options") + ); + } public override void OnLoaded() { @@ -75,8 +93,9 @@ public partial class LaunchOptionsViewModel : ContentDialogViewModelBase public List AsLaunchArgs() { var launchArgs = new List(); - if (Cards is null) return launchArgs; - + if (Cards is null) + return launchArgs; + foreach (var card in Cards) { launchArgs.AddRange(card.Options); diff --git a/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs index 9bafc659..1b8d3131 100644 --- a/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs @@ -33,7 +33,6 @@ using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; -using Notification = Avalonia.Controls.Notifications.Notification; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; @@ -305,13 +304,16 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn IsPrimaryButtonEnabled = true, PrimaryButtonText = "Save", CloseButtonText = "Cancel", + FullSizeDesired = true, DefaultButton = ContentDialogButton.Primary, + ContentMargin = new Thickness(32, 16), Padding = new Thickness(0, 16), Content = new LaunchOptionsDialog { DataContext = viewModel, } }; + var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml index 90f9e431..a9f2bddf 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml @@ -23,7 +23,7 @@ - + From f7d0630b01bccdd742550a11c0ed9122b4dd7e0e Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 24 Aug 2023 16:35:17 -0400 Subject: [PATCH 54/60] Update README --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5c2c9897..7ba2809f 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,17 @@ Multi-Platform Package Manager for Stable Diffusion - Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai] - Embedded Git and Python dependencies, with no need for either to be globally installed - Fully Portable, move Stability Matrix's Data Directory to a new drive or computer at any time + ### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts - Launch arguments editor with predefined or custom options for each Package install -- Package environment variables +- Configurable Environment variables + ### 🗃️ Checkpoint Manager, configured to be shared by all Package installs - Option to find CivitAI metadata and preview thumbnails for new local imports + ### ☁️ Model Browser to import from [CivitAI][civitai] - Automatically imports to the associated model folder depending on the model type -- Also downloads relavent metadata files and preview image +- Also downloads relevant metadata files and preview image ![header](https://github.com/LykosAI/StabilityMatrix/assets/13956642/a9c5f925-8561-49ba-855b-1b7bf57d7c0d) @@ -48,17 +51,17 @@ Multi-Platform Package Manager for Stable Diffusion ### Model browser powered by [Civit AI][civitai] - Downloads new models, automatically uses the appropriate shared model directory -- Available immediately to all installed packages +- Pause and resume downloads, even after closing the app

### Shared model directory for all your packages - - Import local models by simple drag and drop +- Option to find CivitAI metadata and preview thumbnails for new local imports - Toggle visibility of categories like LoRA, VAE, CLIP, etc. -- For models imported from Civit AI, shows additional information like version, fp precision, and preview thumbnail on hover +

From ae5995791e1f18976289ac86d4d1b658bd414b1e Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 24 Aug 2023 16:42:11 -0400 Subject: [PATCH 55/60] Update README for Fooocus --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ba2809f..1d9eaf73 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,14 @@ [sdnext]: https://github.com/vladmandic/automatic [voltaml]: https://github.com/VoltaML/voltaML-fast-stable-diffusion [invokeai]: https://github.com/invoke-ai/InvokeAI +[fooocus]: https://github.com/lllyasviel/Fooocus [civitai]: https://civitai.com/ Multi-Platform Package Manager for Stable Diffusion ### 🖱️ One click install and update for Stable Diffusion Web UI Packages -- Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai] +- Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai], [Fooocus][fooocus] - Embedded Git and Python dependencies, with no need for either to be globally installed - Fully Portable, move Stability Matrix's Data Directory to a new drive or computer at any time From 3884e4b11bf3bd25df5b73fb67c559214840ea91 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 24 Aug 2023 22:57:31 -0700 Subject: [PATCH 56/60] Fix duplicate ProgressItemViewModel properties & make vlad update indeterminate & readme tweaks --- README.md | 8 ++++---- .../ViewModels/ProgressItemViewModel.cs | 6 +----- StabilityMatrix.Core/Models/Packages/VladAutomatic.cs | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1d9eaf73..875ac453 100644 --- a/README.md +++ b/README.md @@ -20,20 +20,20 @@ Multi-Platform Package Manager for Stable Diffusion ### 🖱️ One click install and update for Stable Diffusion Web UI Packages -- Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai], [Fooocus][fooocus] +- Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai], and [Fooocus][fooocus] - Embedded Git and Python dependencies, with no need for either to be globally installed -- Fully Portable, move Stability Matrix's Data Directory to a new drive or computer at any time +- Fully portable; move Stability Matrix's Data Directory to a new drive or computer at any time ### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts - Launch arguments editor with predefined or custom options for each Package install -- Configurable Environment variables +- Configurable Environment Variables ### 🗃️ Checkpoint Manager, configured to be shared by all Package installs - Option to find CivitAI metadata and preview thumbnails for new local imports ### ☁️ Model Browser to import from [CivitAI][civitai] - Automatically imports to the associated model folder depending on the model type -- Also downloads relevant metadata files and preview image +- Downloads relevant metadata files and preview image ![header](https://github.com/LykosAI/StabilityMatrix/assets/13956642/a9c5f925-8561-49ba-855b-1b7bf57d7c0d) diff --git a/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs index 9a511042..50b3dc9a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs @@ -6,12 +6,8 @@ using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Avalonia.ViewModels; -public partial class ProgressItemViewModel : ProgressItemViewModelBase +public class ProgressItemViewModel : ProgressItemViewModelBase { - [ObservableProperty] private Guid id; - [ObservableProperty] private string name; - [ObservableProperty] private bool failed; - public ProgressItemViewModel(ProgressItem progressItem) { Id = progressItem.ProgressId; diff --git a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs index 636cf5ae..8be81362 100644 --- a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs +++ b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs @@ -191,7 +191,7 @@ public class VladAutomatic : BaseGitPackage public override async Task DownloadPackage(string version, bool isCommitHash, string? branch, IProgress? progress = null) { - progress?.Report(new ProgressReport(0.1f, message: "Downloading package...", + progress?.Report(new ProgressReport(-1f, message: "Downloading package...", isIndeterminate: true, type: ProgressType.Download)); var installDir = new DirectoryPath(InstallLocation); @@ -245,7 +245,7 @@ public class VladAutomatic : BaseGitPackage throw new Exception("Installed branch is null"); } - progress?.Report(new ProgressReport(0.1f, message: "Downloading package update...", + progress?.Report(new ProgressReport(-1f, message: "Downloading package update...", isIndeterminate: true, type: ProgressType.Update)); await PrerequisiteHelper.RunGit(installedPackage.FullPath, "checkout", From 0ad89625e7ce6e495fc96e1040f4773e9f4fd497 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 24 Aug 2023 22:58:38 -0700 Subject: [PATCH 57/60] no semicolon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 875ac453..94eeac30 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Multi-Platform Package Manager for Stable Diffusion ### 🖱️ One click install and update for Stable Diffusion Web UI Packages - Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai], and [Fooocus][fooocus] - Embedded Git and Python dependencies, with no need for either to be globally installed -- Fully portable; move Stability Matrix's Data Directory to a new drive or computer at any time +- Fully portable - move Stability Matrix's Data Directory to a new drive or computer at any time ### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts - Launch arguments editor with predefined or custom options for each Package install From 7eff5f3d3eae6c0a39536fecac9acf13893dd2c6 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 24 Aug 2023 23:03:10 -0700 Subject: [PATCH 58/60] also use indeterminate property --- StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs index 50b3dc9a..3e595872 100644 --- a/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs @@ -15,6 +15,7 @@ public class ProgressItemViewModel : ProgressItemViewModelBase Progress.Value = progressItem.Progress.Percentage; Failed = progressItem.Failed; Progress.Text = GetProgressText(progressItem.Progress); + Progress.IsIndeterminate = progressItem.Progress.IsIndeterminate; EventManager.Instance.ProgressChanged += OnProgressChanged; } From 9c9d51d2a90a0dfdc4df6f22844dc6d9e459282c Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 24 Aug 2023 23:03:40 -0700 Subject: [PATCH 59/60] o probably here too --- StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs index 3e595872..9d6cc55e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs @@ -28,6 +28,7 @@ public class ProgressItemViewModel : ProgressItemViewModelBase Progress.Value = e.Progress.Percentage; Failed = e.Failed; Progress.Text = GetProgressText(e.Progress); + Progress.IsIndeterminate = e.Progress.IsIndeterminate; } private string GetProgressText(ProgressReport report) From 2b4b24c3f2044bb88876f78d15b4eb996e54ed01 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 24 Aug 2023 23:15:59 -0700 Subject: [PATCH 60/60] Hide toggle buttons if release mode isn't supported --- StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml index 2332b32d..36961405 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml @@ -135,7 +135,8 @@ TextWrapping="Wrap" IsVisible="{Binding SelectedPackage.Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> - +