From d29067a8d989b539e74d9fb223cc5e0bd659bab7 Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 20 Aug 2023 23:20:28 -0700 Subject: [PATCH 1/9] WIP new page for connected checkpoint updates/management --- StabilityMatrix.Avalonia/App.axaml.cs | 3 + .../DesignData/DesignData.cs | 32 +++ .../CheckpointManager/CheckpointFile.cs | 70 +++++++ .../ViewModels/CheckpointsPageViewModel.cs | 4 +- .../ViewModels/NewCheckpointsPageViewModel.cs | 187 ++++++++++++++++++ .../Views/CheckpointBrowserPage.axaml | 2 +- .../Views/NewCheckpointsPage.axaml | 105 ++++++++++ .../Views/NewCheckpointsPage.axaml.cs | 11 ++ .../Models/Api/CivitModelsRequest.cs | 3 + 9 files changed, 414 insertions(+), 3 deletions(-) create mode 100644 StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/NewCheckpointsPage.axaml create mode 100644 StabilityMatrix.Avalonia/Views/NewCheckpointsPage.axaml.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 487b73a0..374c1407 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -204,6 +204,7 @@ public sealed class App : Application .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); @@ -217,6 +218,7 @@ public sealed class App : Application provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), }, FooterPages = @@ -285,6 +287,7 @@ public sealed class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Dialogs services.AddTransient(); diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index b0fc04ba..a68a7fa4 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -223,6 +223,35 @@ public static class DesignData }) }; + NewCheckpointsPageViewModel.AllCheckpoints = new ObservableCollection + { + new() + { + FilePath = "~/Models/StableDiffusion/electricity-light.safetensors", + Title = "Auroral Background", + PreviewImagePath = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" + + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", + ConnectedModel = new ConnectedModelInfo + { + VersionName = "Lightning Auroral", + BaseModel = "SD 1.5", + ModelName = "Auroral Background", + ModelType = CivitModelType.Model, + FileMetadata = new CivitFileMetadata + { + Format = CivitModelFormat.SafeTensor, + Fp = CivitModelFpType.fp16, + Size = CivitModelSize.pruned, + } + } + }, + new() + { + FilePath = "~/Models/Lora/model.safetensors", + Title = "Some model" + } + }; + ProgressManagerViewModel.ProgressItems = new ObservableCollection { new(new ProgressItem(Guid.NewGuid(), "Test File.exe", @@ -273,6 +302,9 @@ public static class DesignData public static CheckpointsPageViewModel CheckpointsPageViewModel => Services.GetRequiredService(); + public static NewCheckpointsPageViewModel NewCheckpointsPageViewModel => + Services.GetRequiredService(); + public static SettingsViewModel SettingsViewModel => Services.GetRequiredService(); diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs index a8f83521..8ecfff45 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs @@ -14,6 +14,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; @@ -46,6 +47,7 @@ public partial class CheckpointFile : ViewModelBase public bool IsConnectedModel => ConnectedModel != null; [ObservableProperty] private bool isLoading; + [ObservableProperty] private CivitModelType modelType; public string FileName => Path.GetFileName((string?) FilePath); @@ -223,6 +225,39 @@ public partial class CheckpointFile : ViewModelBase yield return checkpointFile; } } + + public static IEnumerable GetAllCheckpointFiles(string modelsDirectory) + { + foreach (var file in Directory.EnumerateFiles(modelsDirectory, "*.*", SearchOption.AllDirectories)) + { + if (!SupportedCheckpointExtensions.Any(ext => file.Contains(ext))) + continue; + + var checkpointFile = new CheckpointFile + { + Title = Path.GetFileNameWithoutExtension(file), + FilePath = file, + }; + + var jsonPath = Path.Combine(Path.GetDirectoryName(file), + Path.GetFileNameWithoutExtension(file) + ".cm-info.json"); + + if (File.Exists(jsonPath)) + { + var json = File.ReadAllText(jsonPath); + var connectedModelInfo = ConnectedModelInfo.FromJson(json); + checkpointFile.ConnectedModel = connectedModelInfo; + checkpointFile.ModelType = GetCivitModelType(file); + } + + checkpointFile.PreviewImagePath = SupportedImageExtensions + .Select(ext => Path.Combine(Path.GetDirectoryName(file), + $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists) + .FirstOrDefault(); + + yield return checkpointFile; + } + } /// /// Index with progress reporting. @@ -238,4 +273,39 @@ public partial class CheckpointFile : ViewModelBase yield return checkpointFile; } } + + private static CivitModelType GetCivitModelType(string filePath) + { + if (filePath.Contains(SharedFolderType.StableDiffusion.ToString())) + { + return CivitModelType.Checkpoint; + } + + if (filePath.Contains(SharedFolderType.ControlNet.ToString())) + { + return CivitModelType.Controlnet; + } + + if (filePath.Contains(SharedFolderType.Lora.ToString())) + { + return CivitModelType.LORA; + } + + if (filePath.Contains(SharedFolderType.TextualInversion.ToString())) + { + return CivitModelType.TextualInversion; + } + + if (filePath.Contains(SharedFolderType.Hypernetwork.ToString())) + { + return CivitModelType.Hypernetwork; + } + + if (filePath.Contains(SharedFolderType.LyCORIS.ToString())) + { + return CivitModelType.LoCon; + } + + return CivitModelType.Unknown; + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs index 19d1e84c..19f99d84 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs @@ -70,7 +70,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase this.downloadService = downloadService; this.modelFinder = modelFinder; } - + public override async Task OnLoadedAsync() { DisplayedCheckpointFolders = CheckpointFolders; @@ -147,7 +147,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase var indexTasks = folders.Select(async f => { var checkpointFolder = - new CheckpointManager.CheckpointFolder(settingsManager, downloadService, modelFinder) + new CheckpointFolder(settingsManager, downloadService, modelFinder) { Title = Path.GetFileName(f), DirectoryPath = f, diff --git a/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs new file mode 100644 index 00000000..da2c4549 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs @@ -0,0 +1,187 @@ +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.Controls.Notifications; +using AvaloniaEdit.Utils; +using CommunityToolkit.Mvvm.ComponentModel; +using FluentAvalonia.UI.Controls; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; +using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Avalonia.Views.Dialogs; +using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Services; +using Symbol = FluentIcons.Common.Symbol; +using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; + +namespace StabilityMatrix.Avalonia.ViewModels; + +[View(typeof(NewCheckpointsPage))] +public partial class NewCheckpointsPageViewModel : PageViewModelBase +{ + private readonly ISettingsManager settingsManager; + private readonly ILiteDbContext liteDbContext; + private readonly ICivitApi civitApi; + private readonly ServiceManager dialogFactory; + private readonly INotificationService notificationService; + public override string Title => "Checkpoint Manager"; + public override IconSource IconSource => new SymbolIconSource + {Symbol = Symbol.Cellular5g, IsFilled = true}; + + public NewCheckpointsPageViewModel(ISettingsManager settingsManager, ILiteDbContext liteDbContext, + ICivitApi civitApi, ServiceManager dialogFactory, INotificationService notificationService) + { + this.settingsManager = settingsManager; + this.liteDbContext = liteDbContext; + this.civitApi = civitApi; + this.dialogFactory = dialogFactory; + this.notificationService = notificationService; + } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectedCheckpoints))] + [NotifyPropertyChangedFor(nameof(NonConnectedCheckpoints))] + private ObservableCollection allCheckpoints = new(); + + [ObservableProperty] + private ObservableCollection civitModels = new(); + + public ObservableCollection ConnectedCheckpoints => new( + AllCheckpoints.Where(x => x.IsConnectedModel) + .OrderBy(x => x.ConnectedModel!.ModelName) + .ThenBy(x => x.ModelType) + .GroupBy(x => x.ConnectedModel!.ModelId) + .Select(x => x.First())); + + public ObservableCollection NonConnectedCheckpoints => new( + AllCheckpoints.Where(x => !x.IsConnectedModel).OrderBy(x => x.ModelType)); + + public override async Task OnLoadedAsync() + { + if (Design.IsDesignMode) return; + + var files = CheckpointFile.GetAllCheckpointFiles(settingsManager.ModelsDirectory); + AllCheckpoints = new ObservableCollection(files); + + var connectedModelIds = ConnectedCheckpoints.Select(x => x.ConnectedModel.ModelId); + var modelRequest = new CivitModelsRequest + { + CommaSeparatedModelIds = string.Join(',', connectedModelIds) + }; + + // See if query is cached + var cachedQuery = await liteDbContext.CivitModelQueryCache + .IncludeAll() + .FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest)); + + // If cached, update model cards + if (cachedQuery is not null) + { + CivitModels = new ObservableCollection(cachedQuery.Items); + + // Start remote query (background mode) + // Skip when last query was less than 2 min ago + var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt; + if (timeSinceCache?.TotalMinutes >= 2) + { + CivitQuery(modelRequest).SafeFireAndForget(); + } + } + else + { + await CivitQuery(modelRequest); + } + } + + public async Task ShowVersionDialog(int modelId) + { + var model = CivitModels.FirstOrDefault(m => m.Id == modelId); + if (model == null) + { + notificationService.Show(new Notification("Model has no versions available", + "This model has no versions available for download", NotificationType.Warning)); + return; + } + var versions = model.ModelVersions; + if (versions is null || versions.Count == 0) + { + notificationService.Show(new Notification("Model has no versions available", + "This model has no versions available for download", NotificationType.Warning)); + return; + } + + var dialog = new BetterContentDialog + { + Title = model.Name, + IsPrimaryButtonEnabled = false, + IsSecondaryButtonEnabled = false, + IsFooterVisible = false, + MaxDialogWidth = 750, + }; + + var viewModel = dialogFactory.Get(); + viewModel.Dialog = dialog; + viewModel.Versions = versions.Select(version => + new ModelVersionViewModel( + settingsManager.Settings.InstalledModelHashes ?? new HashSet(), version)) + .ToImmutableArray(); + viewModel.SelectedVersionViewModel = viewModel.Versions[0]; + + dialog.Content = new SelectModelVersionDialog + { + DataContext = viewModel + }; + + var result = await dialog.ShowAsync(); + + if (result != ContentDialogResult.Primary) + { + return; + } + + var selectedVersion = viewModel?.SelectedVersionViewModel?.ModelVersion; + var selectedFile = viewModel?.SelectedFile?.CivitFile; + } + + private async Task CivitQuery(CivitModelsRequest request) + { + var modelResponse = await civitApi.GetModels(request); + var models = modelResponse.Items; + // Filter out unknown model types and archived/taken-down models + models = models.Where(m => m.Type.ConvertTo() > 0) + .Where(m => m.Mode == null).ToList(); + + // Database update calls will invoke `OnModelsUpdated` + // Add to database + await liteDbContext.UpsertCivitModelAsync(models); + // Add as cache entry + var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync(new CivitModelQueryCacheEntry + { + Id = ObjectHash.GetMd5Guid(request), + InsertedAt = DateTimeOffset.UtcNow, + Request = request, + Items = models, + Metadata = modelResponse.Metadata + }); + + if (cacheNew) + { + CivitModels = new ObservableCollection(models); + } + } +} diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml index a8352c5d..19a3b3ed 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml @@ -40,7 +40,7 @@ Margin="0,8,0,8" Height="300" StretchDirection="Both" - CornerRadius="4" + CornerRadius="8" VerticalContentAlignment="Top" HorizontalContentAlignment="Center" Source="{Binding CardImage}" diff --git a/StabilityMatrix.Avalonia/Views/NewCheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/NewCheckpointsPage.axaml new file mode 100644 index 00000000..05bfb412 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/NewCheckpointsPage.axaml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [AliasAs("baseModels")] public string? BaseModel { get; set; } + + [AliasAs("ids")] + public string CommaSeparatedModelIds { get; set; } } From c9130c4067bf62df889dd7ba33f7ce2093592965 Mon Sep 17 00:00:00 2001 From: JT Date: Mon, 21 Aug 2023 21:05:11 -0700 Subject: [PATCH 2/9] update vlad update to switch back to branch before updating --- .../ViewModels/Dialogs/InstallerViewModel.cs | 8 +-- .../Dialogs/OneClickInstallViewModel.cs | 2 +- .../Models/Packages/BaseGitPackage.cs | 12 ++-- .../Models/Packages/BasePackage.cs | 2 +- .../Models/Packages/InvokeAI.cs | 2 +- .../Models/Packages/VladAutomatic.cs | 57 ++++++++++--------- 6 files changed, 43 insertions(+), 40 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs index 209db2a1..d9f99a7b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs @@ -199,14 +199,14 @@ public partial class InstallerViewModel : ContentDialogViewModelBase version = SelectedVersion?.TagName ?? throw new NullReferenceException("Selected version is null"); - await DownloadPackage(version, false); + await DownloadPackage(version, false, null); } else { version = SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null"); - await DownloadPackage(version, true); + await DownloadPackage(version, true, SelectedVersion!.TagName); } await InstallPackage(); @@ -271,7 +271,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase return branch == null ? version : $"{branch}@{version[..7]}"; } - private Task DownloadPackage(string version, bool isCommitHash) + private Task DownloadPackage(string version, bool isCommitHash, string? branch) { InstallProgress.Text = "Downloading package..."; @@ -282,7 +282,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase EventManager.Instance.OnGlobalProgressChanged((int) progress.Percentage); }); - return SelectedPackage.DownloadPackage(version, isCommitHash, progress); + return SelectedPackage.DownloadPackage(version, isCommitHash, branch, progress); } private async Task InstallPackage() diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs index 854797e4..42208679 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs @@ -157,7 +157,7 @@ public partial class OneClickInstallViewModel : ViewModelBase EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress); }); - await SelectedPackage.DownloadPackage(version, false, progress); + await SelectedPackage.DownloadPackage(version, false, version, progress); SubHeaderText = "Download Complete"; OneClickInstallProgress = 100; } diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index 6dddc6ef..025f87e2 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -139,7 +139,7 @@ public abstract class BaseGitPackage : BasePackage } public override async Task DownloadPackage(string version, bool isCommitHash, - IProgress? progress = null) + string? branch, IProgress? progress = null) { var downloadUrl = GetDownloadUrl(version, isCommitHash); @@ -151,7 +151,7 @@ public abstract class BaseGitPackage : BasePackage await DownloadService .DownloadToFileAsync(downloadUrl, DownloadLocation, progress: progress) .ConfigureAwait(false); - + progress?.Report(new ProgressReport(100, message: "Download Complete")); return version; @@ -246,7 +246,8 @@ public abstract class BaseGitPackage : BasePackage { var releases = await GetAllReleases().ConfigureAwait(false); var latestRelease = releases.First(x => includePrerelease || !x.Prerelease); - await DownloadPackage(latestRelease.TagName, false, progress).ConfigureAwait(false); + await DownloadPackage(latestRelease.TagName, false, null, progress) + .ConfigureAwait(false); await InstallPackage(progress).ConfigureAwait(false); return latestRelease.TagName; } @@ -260,8 +261,9 @@ public abstract class BaseGitPackage : BasePackage { throw new Exception("No commits found for branch"); } - - await DownloadPackage(latestCommit.Sha, true, progress).ConfigureAwait(false); + + await DownloadPackage(latestCommit.Sha, true, installedPackage.InstalledBranch, progress) + .ConfigureAwait(false); await InstallPackage(progress).ConfigureAwait(false); return latestCommit.Sha; } diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs index 285e5b10..06cff8a4 100644 --- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs @@ -33,7 +33,7 @@ public abstract class BasePackage public virtual bool ShouldIgnoreReleases => false; public virtual bool UpdateAvailable { get; set; } - public abstract Task DownloadPackage(string version, bool isCommitHash, + public abstract Task DownloadPackage(string version, bool isCommitHash, string? branch, IProgress? progress = null); public abstract Task InstallPackage(IProgress? progress = null); public abstract Task RunPackage(string installedPackagePath, string command, string arguments); diff --git a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs index 7ac7592a..cdc26769 100644 --- a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs +++ b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs @@ -116,7 +116,7 @@ public class InvokeAI : BaseGitPackage public override Task GetLatestVersion() => Task.FromResult("main"); - public override Task DownloadPackage(string version, bool isCommitHash, + public override Task DownloadPackage(string version, bool isCommitHash, string? branch, IProgress? progress = null) { return Task.FromResult(version); diff --git a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs index 679840ee..636cf5ae 100644 --- a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs +++ b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs @@ -188,20 +188,22 @@ public class VladAutomatic : BaseGitPackage progress?.Report(new ProgressReport(1, isIndeterminate: false)); } - public override async Task DownloadPackage(string version, bool isCommitHash, IProgress? progress = null) + public override async Task DownloadPackage(string version, bool isCommitHash, + string? branch, IProgress? progress = null) { - progress?.Report(new ProgressReport(0.1f, message: "Downloading package...", isIndeterminate: true, type: ProgressType.Download)); - + progress?.Report(new ProgressReport(0.1f, message: "Downloading package...", + isIndeterminate: true, type: ProgressType.Download)); + var installDir = new DirectoryPath(InstallLocation); installDir.Create(); - - await PrerequisiteHelper.RunGit( - installDir.Parent ?? "", "clone", "https://github.com/vladmandic/automatic", installDir.Name) - .ConfigureAwait(false); - + + await PrerequisiteHelper + .RunGit(installDir.Parent ?? "", "clone", "https://github.com/vladmandic/automatic", + installDir.Name).ConfigureAwait(false); + await PrerequisiteHelper.RunGit( InstallLocation, "checkout", version).ConfigureAwait(false); - + return version; } @@ -244,15 +246,18 @@ public class VladAutomatic : BaseGitPackage } progress?.Report(new ProgressReport(0.1f, message: "Downloading package update...", - isIndeterminate: true, type: ProgressType.Download)); + isIndeterminate: true, type: ProgressType.Update)); - var version = await GithubApi.GetAllCommits(Author, Name, installedPackage.InstalledBranch).ConfigureAwait(false); - var latest = version?.FirstOrDefault(); + await PrerequisiteHelper.RunGit(installedPackage.FullPath, "checkout", + installedPackage.InstalledBranch).ConfigureAwait(false); + + var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath!, "venv")); + venvRunner.WorkingDirectory = InstallLocation; + venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables; + + await venvRunner.CustomInstall("launch.py --upgrade --test", OnConsoleOutput) + .ConfigureAwait(false); - if (latest?.Sha is null) - { - throw new Exception("Could not get latest version"); - } try { @@ -261,22 +266,18 @@ public class VladAutomatic : BaseGitPackage .GetGitOutput(installedPackage.FullPath, "rev-parse", "HEAD") .ConfigureAwait(false); - if (output.Replace(Environment.NewLine, "") == latest.Sha) - { - return latest.Sha; - } + return output.Replace(Environment.NewLine, "").Replace("\n", ""); } catch (Exception e) { Logger.Warn(e, "Could not get current git hash, continuing with update"); } - - await PrerequisiteHelper.RunGit(installedPackage.FullPath, "pull", - "origin", installedPackage.InstalledBranch).ConfigureAwait(false); - - progress?.Report(new ProgressReport(1f, message: "Update Complete", isIndeterminate: false, - type: ProgressType.Generic)); - - return latest.Sha; + finally + { + progress?.Report(new ProgressReport(1f, message: "Update Complete", isIndeterminate: false, + type: ProgressType.Update)); + } + + return installedPackage.InstalledBranch; } } From e62111c788285d57d7726cb3e34200f1256c1c89 Mon Sep 17 00:00:00 2001 From: JT Date: Mon, 21 Aug 2023 23:38:31 -0700 Subject: [PATCH 3/9] Speed up CheckpointsPage some more & add "installed" sortmode to model browser & hide NewCheckpointsPage until its more finished --- StabilityMatrix.Avalonia/App.axaml.cs | 1 - .../DesignData/DesignData.cs | 5 ++ .../ViewModels/CheckpointBrowserViewModel.cs | 34 ++++++++- .../CheckpointManager/CheckpointFolder.cs | 4 +- .../ViewModels/CheckpointsPageViewModel.cs | 32 +++++--- .../ViewModels/NewCheckpointsPageViewModel.cs | 76 ++++++++++++++----- .../Views/CheckpointsPage.axaml | 40 +++++----- .../Models/Api/CivitModelsRequest.cs | 15 ++++ .../Models/Api/CivitSortMode.cs | 4 +- 9 files changed, 152 insertions(+), 59 deletions(-) diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 374c1407..2eee72fc 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -218,7 +218,6 @@ public sealed class App : Application provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService(), provider.GetRequiredService(), }, FooterPages = diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index a68a7fa4..737ce0af 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -192,6 +192,11 @@ public static class DesignData { Title = "StableDiffusion", DirectoryPath = "Packages/Lora/Subfolder", + }, + new(settingsManager, downloadService, modelFinder) + { + Title = "Lora", + DirectoryPath = "Packages/StableDiffusion/Subfolder", } }, CheckpointFiles = new AdvancedObservableList diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs index 53abad57..55245738 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs @@ -17,6 +17,7 @@ using Refit; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; +using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; @@ -63,7 +64,7 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase [ObservableProperty] private bool isIndeterminate; [ObservableProperty] private bool noResultsFound; [ObservableProperty] private string noResultsText = string.Empty; - [ObservableProperty] private string selectedBaseModelType = "All"; + [ObservableProperty] private string selectedBaseModelType = "All"; private List allModelCards = new(); @@ -250,9 +251,15 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase }).ToList(); allModelCards = updateCards; - ModelCards = - new ObservableCollection( - updateCards.Where(FilterModelCardsPredicate)); + + var filteredCards = updateCards.Where(FilterModelCardsPredicate); + if (SortMode == CivitSortMode.Installed) + { + filteredCards = + filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available"); + } + + ModelCards =new ObservableCollection(filteredCards); } TotalPages = metadata?.TotalPages ?? 1; CanGoToPreviousPage = CurrentPageNumber > 1; @@ -309,6 +316,25 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase { modelRequest.BaseModel = SelectedBaseModelType; } + + if (SortMode == CivitSortMode.Installed) + { + var connectedModels = + CheckpointFile.GetAllCheckpointFiles(settingsManager.ModelsDirectory) + .Where(c => c.IsConnectedModel); + + if (SelectedModelType != CivitModelType.All) + { + connectedModels = connectedModels.Where(c => c.ModelType == SelectedModelType); + } + + modelRequest = new CivitModelsRequest + { + CommaSeparatedModelIds = string.Join(",", + connectedModels.Select(c => c.ConnectedModel!.ModelId)), + Types = SelectedModelType == CivitModelType.All ? null : new[] {SelectedModelType} + }; + } // See if query is cached var cachedQuery = await liteDbContext.CivitModelQueryCache diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs index 2808d564..eaf8eadb 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -414,8 +415,7 @@ public partial class CheckpointFolder : ViewModelBase { // Create subfolder var subFolder = new CheckpointFolder(settingsManager, - downloadService, modelFinder, - useCategoryVisibility: false) + downloadService, modelFinder, useCategoryVisibility: false) { Title = Path.GetFileName(folder), DirectoryPath = folder, diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs index 19f99d84..b3351ebc 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs @@ -73,28 +73,31 @@ public partial class CheckpointsPageViewModel : PageViewModelBase public override async Task OnLoadedAsync() { + var sw = Stopwatch.StartNew(); DisplayedCheckpointFolders = CheckpointFolders; // Set UI states IsImportAsConnected = settingsManager.Settings.IsImportAsConnected; // Refresh search filter OnSearchFilterChanged(string.Empty); + + Logger.Info($"Loaded {DisplayedCheckpointFolders.Count} checkpoint folders in {sw.ElapsedMilliseconds}ms"); if (Design.IsDesignMode) return; - await Dispatcher.UIThread.InvokeAsync(async () => - { - IsLoading = CheckpointFolders.Count == 0; - IsIndexing = CheckpointFolders.Count > 0; - await IndexFolders(); - IsLoading = false; - IsIndexing = false; - }); + IsLoading = CheckpointFolders.Count == 0; + IsIndexing = CheckpointFolders.Count > 0; + await IndexFolders(); + IsLoading = false; + IsIndexing = false; + + Logger.Info($"OnLoadedAsync in {sw.ElapsedMilliseconds}ms"); } // ReSharper disable once UnusedParameterInPartialMethod partial void OnSearchFilterChanged(string value) { + var sw = Stopwatch.StartNew(); if (string.IsNullOrWhiteSpace(SearchFilter)) { DisplayedCheckpointFolders = new ObservableCollection( @@ -103,15 +106,21 @@ public partial class CheckpointsPageViewModel : PageViewModelBase x.SearchFilter = SearchFilter; return x; })); + sw.Stop(); + Logger.Info($"OnSearchFilterChanged in {sw.ElapsedMilliseconds}ms"); return; } + sw.Restart(); + var filteredFolders = CheckpointFolders .Where(ContainsSearchFilter).ToList(); foreach (var folder in filteredFolders) { folder.SearchFilter = SearchFilter; } + sw.Stop(); + Logger.Info($"ContainsSearchFilter in {sw.ElapsedMilliseconds}ms"); DisplayedCheckpointFolders = new ObservableCollection(filteredFolders); } @@ -143,6 +152,8 @@ public partial class CheckpointsPageViewModel : PageViewModelBase var folders = Directory.GetDirectories(modelsDirectory); + var sw = Stopwatch.StartNew(); + // Index all folders var indexTasks = folders.Select(async f => { @@ -158,13 +169,16 @@ public partial class CheckpointsPageViewModel : PageViewModelBase }).ToList(); await Task.WhenAll(indexTasks); + + sw.Stop(); + Logger.Info($"IndexFolders in {sw.ElapsedMilliseconds}ms"); // Set new observable collection, ordered by alphabetical order CheckpointFolders = new ObservableCollection(indexTasks .Select(t => t.Result) .OrderBy(f => f.Title)); - + if (!string.IsNullOrWhiteSpace(SearchFilter)) { var filtered = CheckpointFolders diff --git a/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs index da2c4549..b165aef0 100644 --- a/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/NewCheckpointsPageViewModel.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; @@ -11,6 +12,8 @@ using Avalonia.Controls.Notifications; using AvaloniaEdit.Utils; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; +using Microsoft.Extensions.Logging; +using Refit; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -34,6 +37,7 @@ namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(NewCheckpointsPage))] public partial class NewCheckpointsPageViewModel : PageViewModelBase { + private readonly ILogger logger; private readonly ISettingsManager settingsManager; private readonly ILiteDbContext liteDbContext; private readonly ICivitApi civitApi; @@ -43,9 +47,11 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase public override IconSource IconSource => new SymbolIconSource {Symbol = Symbol.Cellular5g, IsFilled = true}; - public NewCheckpointsPageViewModel(ISettingsManager settingsManager, ILiteDbContext liteDbContext, - ICivitApi civitApi, ServiceManager dialogFactory, INotificationService notificationService) + public NewCheckpointsPageViewModel(ILogger logger, + ISettingsManager settingsManager, ILiteDbContext liteDbContext, ICivitApi civitApi, + ServiceManager dialogFactory, INotificationService notificationService) { + this.logger = logger; this.settingsManager = settingsManager; this.liteDbContext = liteDbContext; this.civitApi = civitApi; @@ -160,28 +166,56 @@ public partial class NewCheckpointsPageViewModel : PageViewModelBase private async Task CivitQuery(CivitModelsRequest request) { - var modelResponse = await civitApi.GetModels(request); - var models = modelResponse.Items; - // Filter out unknown model types and archived/taken-down models - models = models.Where(m => m.Type.ConvertTo() > 0) - .Where(m => m.Mode == null).ToList(); - - // Database update calls will invoke `OnModelsUpdated` - // Add to database - await liteDbContext.UpsertCivitModelAsync(models); - // Add as cache entry - var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync(new CivitModelQueryCacheEntry + try { - Id = ObjectHash.GetMd5Guid(request), - InsertedAt = DateTimeOffset.UtcNow, - Request = request, - Items = models, - Metadata = modelResponse.Metadata - }); + var modelResponse = await civitApi.GetModels(request); + var models = modelResponse.Items; + // Filter out unknown model types and archived/taken-down models + models = models.Where(m => m.Type.ConvertTo() > 0) + .Where(m => m.Mode == null).ToList(); + + // Database update calls will invoke `OnModelsUpdated` + // Add to database + await liteDbContext.UpsertCivitModelAsync(models); + // Add as cache entry + var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync( + new CivitModelQueryCacheEntry + { + Id = ObjectHash.GetMd5Guid(request), + InsertedAt = DateTimeOffset.UtcNow, + Request = request, + Items = models, + Metadata = modelResponse.Metadata + }); - if (cacheNew) + if (cacheNew) + { + CivitModels = new ObservableCollection(models); + } + } + catch (OperationCanceledException) + { + notificationService.Show(new Notification("Request to CivitAI timed out", + "Could not check for checkpoint updates. Please try again later.")); + logger.LogWarning($"CivitAI query timed out ({request})"); + } + catch (HttpRequestException e) + { + notificationService.Show(new Notification("CivitAI can't be reached right now", + "Could not check for checkpoint updates. Please try again later.")); + logger.LogWarning(e, $"CivitAI query HttpRequestException ({request})"); + } + catch (ApiException e) + { + notificationService.Show(new Notification("CivitAI can't be reached right now", + "Could not check for checkpoint updates. Please try again later.")); + logger.LogWarning(e, $"CivitAI query ApiException ({request})"); + } + catch (Exception e) { - CivitModels = new ObservableCollection(models); + notificationService.Show(new Notification("CivitAI can't be reached right now", + $"Unknown exception during CivitAI query: {e.GetType().Name}")); + logger.LogError(e, $"CivitAI query unknown exception ({request})"); } } } diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index bb98b979..21178818 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -239,17 +239,19 @@ - + - - - + + + + + - - - - - + Text="Drag & drop checkpoints here to import" + IsVisible="{Binding !CheckpointFiles.Count}"/> + IsVisible="{Binding Progress.IsTextVisible}" /> + IsVisible="{Binding Progress.IsProgressVisible}" + Value="{Binding Progress.Value, FallbackValue=20}" /> - + diff --git a/StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs b/StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs index 3e40f80e..66ea7c48 100644 --- a/StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs +++ b/StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs @@ -120,4 +120,19 @@ public class CivitModelsRequest [AliasAs("ids")] public string CommaSeparatedModelIds { get; set; } + + public override string ToString() + { + return $"Page: {Page}, " + + $"Query: {Query}, " + + $"Tag: {Tag}, " + + $"Username: {Username}, " + + $"Types: {Types}, " + + $"Sort: {Sort}, " + + $"Period: {Period}, " + + $"Rating: {Rating}, " + + $"Nsfw: {Nsfw}, " + + $"BaseModel: {BaseModel}, " + + $"CommaSeparatedModelIds: {CommaSeparatedModelIds}"; + } } diff --git a/StabilityMatrix.Core/Models/Api/CivitSortMode.cs b/StabilityMatrix.Core/Models/Api/CivitSortMode.cs index 6c522865..241f7ff6 100644 --- a/StabilityMatrix.Core/Models/Api/CivitSortMode.cs +++ b/StabilityMatrix.Core/Models/Api/CivitSortMode.cs @@ -11,5 +11,7 @@ public enum CivitSortMode [EnumMember(Value = "Most Downloaded")] MostDownloaded, [EnumMember(Value = "Newest")] - Newest + Newest, + [EnumMember(Value = "Installed")] + Installed, } From 28d1c595eb4b3d22af9a29fc5d2510dffd2e10f2 Mon Sep 17 00:00:00 2001 From: JT Date: Mon, 21 Aug 2023 23:58:23 -0700 Subject: [PATCH 4/9] Update chagenlog & group IDs --- CHANGELOG.md | 8 +++++--- .../ViewModels/CheckpointBrowserViewModel.cs | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7a10ae..dc54091e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,18 +5,20 @@ All notable changes to Stability Matrix will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). -## v2.2.1 +## v2.3.0 ### 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 - Fixed issue where model version wouldn't be selected in the "All Versions" section of the Model Browser - Improved Checkpoints page indexing performance - 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 +## v2.2.1 +### Fixed +- Fixed SD.Next shared folders config not working with new config format, reverted to Junctions / Symlinks + ## v2.2.0 ### Added - Added option to search by Base Model in the Model Browser diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs index 55245738..5e332efe 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs @@ -327,11 +327,12 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase { connectedModels = connectedModels.Where(c => c.ModelType == SelectedModelType); } - + modelRequest = new CivitModelsRequest { CommaSeparatedModelIds = string.Join(",", - connectedModels.Select(c => c.ConnectedModel!.ModelId)), + connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m) + .Select(g => g.First())), Types = SelectedModelType == CivitModelType.All ? null : new[] {SelectedModelType} }; } From 006d84489098c32b2d60c1a08e789ed8fb0f6577 Mon Sep 17 00:00:00 2001 From: JT Date: Tue, 22 Aug 2023 00:02:25 -0700 Subject: [PATCH 5/9] chagenlog again --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc54091e..8c5fd4a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Improved Checkpoints page indexing performance - 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 +### Changed +- Changed update method for SD.Next to use the built-in upgrade functionality ## v2.2.1 ### Fixed From 4409f6f3123545bbac2e76038d091e0bb1f46bb5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 22 Aug 2023 18:55:36 -0400 Subject: [PATCH 6/9] Add localization file and settings (debug only) --- StabilityMatrix.Avalonia/App.axaml.cs | 12 +- StabilityMatrix.Avalonia/Controls/AutoGrid.cs | 408 ++++++++++ .../Languages/Cultures.cs | 52 ++ .../Languages/Resources.ja-JP.resx | 23 + .../Languages/Resources.resx | 48 ++ .../StabilityMatrix.Avalonia.csproj | 16 + .../ViewModels/SettingsViewModel.cs | 46 ++ .../Views/SettingsPage.axaml | 700 +++++++++--------- .../Models/Settings/Settings.cs | 3 +- 9 files changed, 967 insertions(+), 341 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/AutoGrid.cs create mode 100644 StabilityMatrix.Avalonia/Languages/Cultures.cs create mode 100644 StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx create mode 100644 StabilityMatrix.Avalonia/Languages/Resources.resx diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 36529dae..95079557 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -34,11 +34,13 @@ using Sentry; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.DesignData; using StabilityMatrix.Avalonia.Helpers; +using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; 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.Avalonia.Views; @@ -57,8 +59,7 @@ using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; using Application = Avalonia.Application; -using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile; -using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder; +using Language = StabilityMatrix.Avalonia.Languages.Language; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace StabilityMatrix.Avalonia; @@ -194,7 +195,12 @@ public sealed class App : Application Services = services.BuildServiceProvider(); var settingsManager = Services.GetRequiredService(); - settingsManager.TryFindLibrary(); + + if (settingsManager.TryFindLibrary()) + { + Cultures.TrySetSupportedCulture(settingsManager.Settings.Language); + } + Services.GetRequiredService().StartEventListener(); } diff --git a/StabilityMatrix.Avalonia/Controls/AutoGrid.cs b/StabilityMatrix.Avalonia/Controls/AutoGrid.cs new file mode 100644 index 00000000..df5c4735 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/AutoGrid.cs @@ -0,0 +1,408 @@ +// Modified from https://github.com/AvaloniaUI/AvaloniaAutoGrid +/*The MIT License (MIT) + +Copyright (c) 2013 Charles Brown (carbonrobot) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.*/ + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Layout; + +namespace StabilityMatrix.Avalonia.Controls; + +/// +/// Defines a flexible grid area that consists of columns and rows. +/// Depending on the orientation, either the rows or the columns are auto-generated, +/// and the children's position is set according to their index. +/// +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class AutoGrid : Grid +{ + /// + /// Gets or sets the child horizontal alignment. + /// + /// The child horizontal alignment. + [Category("Layout"), Description("Presets the horizontal alignment of all child controls")] + public HorizontalAlignment? ChildHorizontalAlignment + { + get => (HorizontalAlignment?)GetValue(ChildHorizontalAlignmentProperty); + set => SetValue(ChildHorizontalAlignmentProperty, value); + } + + /// + /// Gets or sets the child margin. + /// + /// The child margin. + [Category("Layout"), Description("Presets the margin of all child controls")] + public Thickness? ChildMargin + { + get => (Thickness?)GetValue(ChildMarginProperty); + set => SetValue(ChildMarginProperty, value); + } + + /// + /// Gets or sets the child vertical alignment. + /// + /// The child vertical alignment. + [Category("Layout"), Description("Presets the vertical alignment of all child controls")] + public VerticalAlignment? ChildVerticalAlignment + { + get => (VerticalAlignment?)GetValue(ChildVerticalAlignmentProperty); + set => SetValue(ChildVerticalAlignmentProperty, value); + } + + /// + /// Gets or sets the column count + /// + [Category("Layout"), Description("Defines a set number of columns")] + public int ColumnCount + { + get => (int)GetValue(ColumnCountProperty)!; + set => SetValue(ColumnCountProperty, value); + } + + /// + /// Gets or sets the fixed column width + /// + [Category("Layout"), Description("Presets the width of all columns set using the ColumnCount property")] + + public GridLength ColumnWidth + { + get => (GridLength)GetValue(ColumnWidthProperty)!; + set => SetValue(ColumnWidthProperty, value); + } + + /// + /// Gets or sets a value indicating whether the children are automatically indexed. + /// + /// The default is true. + /// Note that if children are already indexed, setting this property to false will not remove their indices. + /// + /// + [Category("Layout"), Description("Set to false to disable the auto layout functionality")] + public bool IsAutoIndexing + { + get => (bool)GetValue(IsAutoIndexingProperty)!; + set => SetValue(IsAutoIndexingProperty, value); + } + + /// + /// Gets or sets the orientation. + /// The default is Vertical. + /// + /// The orientation. + [Category("Layout"), Description("Defines the directionality of the autolayout. Use vertical for a column first layout, horizontal for a row first layout.")] + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty)!; + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets the number of rows + /// + [Category("Layout"), Description("Defines a set number of rows")] + public int RowCount + { + get => (int)GetValue(RowCountProperty)!; + set => SetValue(RowCountProperty, value); + } + + /// + /// Gets or sets the fixed row height + /// + [Category("Layout"), Description("Presets the height of all rows set using the RowCount property")] + public GridLength RowHeight + { + get => (GridLength)GetValue(RowHeightProperty)!; + set => SetValue(RowHeightProperty, value); + } + + /// + /// Handles the column count changed event + /// + public static void ColumnCountChanged(AvaloniaPropertyChangedEventArgs e) + { + if ((int)e.NewValue! < 0) + return; + + var grid = (AutoGrid)e.Sender; + + + // look for an existing column definition for the height + var width = grid.ColumnWidth; + if (!grid.IsSet(ColumnWidthProperty) && grid.ColumnDefinitions.Count > 0) + width = grid.ColumnDefinitions[0].Width; + + // clear and rebuild + grid.ColumnDefinitions.Clear(); + for (var i = 0; i < (int)e.NewValue; i++) + grid.ColumnDefinitions.Add( + new ColumnDefinition() { Width = width }); + } + + /// + /// Handle the fixed column width changed event + /// + public static void FixedColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) + { + var grid = (AutoGrid)e.Sender; + + // add a default column if missing + if (grid.ColumnDefinitions.Count == 0) + grid.ColumnDefinitions.Add(new ColumnDefinition()); + + // set all existing columns to this width + foreach (var t in grid.ColumnDefinitions) + t.Width = (GridLength)e.NewValue!; + } + + /// + /// Handle the fixed row height changed event + /// + public static void FixedRowHeightChanged(AvaloniaPropertyChangedEventArgs e) + { + var grid = (AutoGrid)e.Sender; + + // add a default row if missing + if (grid.RowDefinitions.Count == 0) + grid.RowDefinitions.Add(new RowDefinition()); + + // set all existing rows to this height + foreach (var t in grid.RowDefinitions) + t.Height = (GridLength)e.NewValue!; + } + + /// + /// Handles the row count changed event + /// + public static void RowCountChanged(AvaloniaPropertyChangedEventArgs e) + { + if ((int)e.NewValue! < 0) + return; + + var grid = (AutoGrid)e.Sender; + + // look for an existing row to get the height + var height = grid.RowHeight; + if (!grid.IsSet(RowHeightProperty) && grid.RowDefinitions.Count > 0) + height = grid.RowDefinitions[0].Height; + + // clear and rebuild + grid.RowDefinitions.Clear(); + for (var i = 0; i < (int)e.NewValue; i++) + grid.RowDefinitions.Add( + new RowDefinition() { Height = height }); + } + + /// + /// Called when [child horizontal alignment changed]. + /// + private static void OnChildHorizontalAlignmentChanged(AvaloniaPropertyChangedEventArgs e) + { + var grid = (AutoGrid)e.Sender; + foreach (var child in grid.Children) + { + child.SetValue(HorizontalAlignmentProperty, + grid.ChildHorizontalAlignment ?? AvaloniaProperty.UnsetValue); + } + } + + /// + /// Called when [child layout changed]. + /// + private static void OnChildMarginChanged(AvaloniaPropertyChangedEventArgs e) + { + var grid = (AutoGrid)e.Sender; + foreach (var child in grid.Children) + { + child.SetValue(MarginProperty, grid.ChildMargin ?? AvaloniaProperty.UnsetValue); + } + } + + /// + /// Called when [child vertical alignment changed]. + /// + private static void OnChildVerticalAlignmentChanged(AvaloniaPropertyChangedEventArgs e) + { + var grid = (AutoGrid)e.Sender; + foreach (var child in grid.Children) + { + child.SetValue(VerticalAlignmentProperty, grid.ChildVerticalAlignment ?? AvaloniaProperty.UnsetValue); + } + } + + /// + /// Apply child margins and layout effects such as alignment + /// + private void ApplyChildLayout(Control child) + { + if (ChildMargin != null) + { + child.SetValue(MarginProperty, ChildMargin.Value, BindingPriority.Template); + } + if (ChildHorizontalAlignment != null) + { + child.SetValue(HorizontalAlignmentProperty, ChildHorizontalAlignment.Value, BindingPriority.Template); + } + if (ChildVerticalAlignment != null) + { + child.SetValue(VerticalAlignmentProperty, ChildVerticalAlignment.Value, BindingPriority.Template); + } + } + + /// + /// Clamp a value to its maximum. + /// + private int Clamp(int value, int max) + { + return (value > max) ? max : value; + } + + /// + /// Perform the grid layout of row and column indexes + /// + private void PerformLayout() + { + var fillRowFirst = Orientation == Orientation.Horizontal; + var rowCount = RowDefinitions.Count; + var colCount = ColumnDefinitions.Count; + + if (rowCount == 0 || colCount == 0) + return; + + var position = 0; + var skip = new bool[rowCount, colCount]; + foreach (var child in Children.OfType()) + { + var childIsCollapsed = !child.IsVisible; + if (IsAutoIndexing && !childIsCollapsed) + { + if (fillRowFirst) + { + var row = Clamp(position / colCount, rowCount - 1); + var col = Clamp(position % colCount, colCount - 1); + if (skip[row, col]) + { + position++; + row = (position / colCount); + col = (position % colCount); + } + + SetRow(child, row); + SetColumn(child, col); + position += GetColumnSpan(child); + + var offset = GetRowSpan(child) - 1; + while (offset > 0) + { + skip[row + offset--, col] = true; + } + } + else + { + var row = Clamp(position % rowCount, rowCount - 1); + var col = Clamp(position / rowCount, colCount - 1); + if (skip[row, col]) + { + position++; + row = position % rowCount; + col = position / rowCount; + } + + SetRow(child, row); + SetColumn(child, col); + position += GetRowSpan(child); + + var offset = GetColumnSpan(child) - 1; + while (offset > 0) + { + skip[row, col + offset--] = true; + } + } + } + + ApplyChildLayout(child); + } + } + + public static readonly AvaloniaProperty ChildHorizontalAlignmentProperty = + AvaloniaProperty.Register("ChildHorizontalAlignment"); + + public static readonly AvaloniaProperty ChildMarginProperty = + AvaloniaProperty.Register("ChildMargin"); + + public static readonly AvaloniaProperty ChildVerticalAlignmentProperty = + AvaloniaProperty.Register("ChildVerticalAlignment"); + + public static readonly AvaloniaProperty ColumnCountProperty = + AvaloniaProperty.RegisterAttached("ColumnCount", typeof(AutoGrid), 1); + + public static readonly AvaloniaProperty ColumnWidthProperty = + AvaloniaProperty.RegisterAttached("ColumnWidth", typeof(AutoGrid), GridLength.Auto); + + public static readonly AvaloniaProperty IsAutoIndexingProperty = + AvaloniaProperty.Register("IsAutoIndexing", true); + + public static readonly AvaloniaProperty OrientationProperty = + AvaloniaProperty.Register("Orientation", Orientation.Vertical); + + public static readonly AvaloniaProperty RowCountProperty = + AvaloniaProperty.RegisterAttached("RowCount", typeof(AutoGrid), 1); + + public static readonly AvaloniaProperty RowHeightProperty = + AvaloniaProperty.RegisterAttached("RowHeight", typeof(AutoGrid), GridLength.Auto); + + static AutoGrid() + { + AffectsMeasure(ChildHorizontalAlignmentProperty, ChildMarginProperty, + ChildVerticalAlignmentProperty, ColumnCountProperty, ColumnWidthProperty, IsAutoIndexingProperty, OrientationProperty, + RowHeightProperty); + + ChildHorizontalAlignmentProperty.Changed.Subscribe(OnChildHorizontalAlignmentChanged); + ChildMarginProperty.Changed.Subscribe(OnChildMarginChanged); + ChildVerticalAlignmentProperty.Changed.Subscribe(OnChildVerticalAlignmentChanged); + ColumnCountProperty.Changed.Subscribe(ColumnCountChanged); + RowCountProperty.Changed.Subscribe(RowCountChanged); + ColumnWidthProperty.Changed.Subscribe(FixedColumnWidthChanged); + RowHeightProperty.Changed.Subscribe(FixedRowHeightChanged); + } + + #region Overrides + + /// + /// Measures the children of a in anticipation of arranging them during the pass. + /// + /// Indicates an upper limit size that should not be exceeded. + /// + /// that represents the required size to arrange child content. + /// + protected override Size MeasureOverride(Size constraint) + { + PerformLayout(); + return base.MeasureOverride(constraint); + } + + #endregion Overrides +} diff --git a/StabilityMatrix.Avalonia/Languages/Cultures.cs b/StabilityMatrix.Avalonia/Languages/Cultures.cs new file mode 100644 index 00000000..542fd18b --- /dev/null +++ b/StabilityMatrix.Avalonia/Languages/Cultures.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace StabilityMatrix.Avalonia.Languages; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public static class Cultures +{ + public static CultureInfo Default { get; } = new("en-US"); + + public static CultureInfo Current => Resources.Culture; + + public static readonly Dictionary SupportedCulturesByCode = + new Dictionary + { + ["en-US"] = Default, + ["ja-JP"] = new("ja-JP") + }; + + public static IReadOnlyList SupportedCultures + => SupportedCulturesByCode.Values.ToImmutableList(); + + public static CultureInfo GetSupportedCultureOrDefault(string? cultureCode) + { + if (cultureCode is null + || !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture)) + { + return Default; + } + + return culture; + } + + public static bool TrySetSupportedCulture(string? cultureCode) + { + if (cultureCode is null + || !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture)) + { + return false; + } + + Resources.Culture = culture; + return true; + } + + public static bool TrySetSupportedCulture(CultureInfo? cultureInfo) + { + return cultureInfo is not null && TrySetSupportedCulture(cultureInfo.Name); + } +} diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx new file mode 100644 index 00000000..ccd583bf --- /dev/null +++ b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx @@ -0,0 +1,23 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 保存 + + + 戻る + + + 言語 + + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx new file mode 100644 index 00000000..fa598c67 --- /dev/null +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -0,0 +1,48 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Launch + + + Quit + + + Save + + + Cancel + + + Language + + + Relaunch is required for new language option to take effect + + + Relaunch + + + Relaunch Later + + + Relaunch Required + + diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index f9d7a498..4c6a2f6e 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -76,4 +76,20 @@ + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index f52877c7..ba33442d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -12,12 +13,14 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.Notifications; using Avalonia.Styling; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Helpers; +using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -67,6 +70,11 @@ public partial class SettingsViewModel : PageViewModelBase "System", }; + [ObservableProperty] private CultureInfo selectedLanguage; + + // ReSharper disable once MemberCanBeMadeStatic.Global + public IReadOnlyList AvailableLanguages => Cultures.SupportedCultures; + public IReadOnlyList AnimationScaleOptions { get; } = new[] { 0f, @@ -118,6 +126,7 @@ public partial class SettingsViewModel : PageViewModelBase SharedState = sharedState; SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1]; + SelectedLanguage = Cultures.GetSupportedCultureOrDefault(settingsManager.Settings.Language); RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown; SelectedAnimationScale = settingsManager.Settings.AnimationScale; @@ -146,6 +155,43 @@ public partial class SettingsViewModel : PageViewModelBase _ => ThemeVariant.Default }; } + + partial void OnSelectedLanguageChanged(CultureInfo? oldValue, CultureInfo newValue) + { + if (oldValue is null || newValue.Name == Cultures.Current.Name) return; + // Set locale + if (AvailableLanguages.Contains(newValue)) + { + Logger.Info("Changing language from {Old} to {New}", + oldValue, newValue); + + Cultures.TrySetSupportedCulture(newValue); + settingsManager.Transaction(s => s.Language = newValue.Name); + + var dialog = new BetterContentDialog + { + Title = Resources.Label_RelaunchRequired, + Content = Resources.Text_RelaunchRequiredToApplyLanguage, + DefaultButton = ContentDialogButton.Primary, + PrimaryButtonText = Resources.Action_Relaunch, + CloseButtonText = Resources.Action_RelaunchLater + }; + + Dispatcher.UIThread.InvokeAsync(async () => + { + if (await dialog.ShowAsync() == ContentDialogResult.Primary) + { + Process.Start(Compat.AppCurrentPath); + App.Shutdown(); + } + }); + } + else + { + Logger.Info("Requested invalid language change from {Old} to {New}", + oldValue, newValue); + } + } partial void OnRemoveSymlinksOnShutdownChanged(bool value) { diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index f9e8c09b..cb745ba9 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -1,342 +1,368 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +