diff --git a/CHANGELOG.md b/CHANGELOG.md index fde21793..1d1c5d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,15 +9,20 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 ### Added - Added Inference - A native generation interface that connects with a running ComfyUI Package as backend. - New installable Package - [Fooocus-MRE](https://github.com/MoonRide303/Fooocus-MRE) +- Added toggle to show connected model images in the Checkpoints tab +- Added "Find Connected Metadata" option to the context menu of Checkpoint Folders in the Checkpoints tab to connect models that don't have any metadata ### Changed - Revamped package installer - Added "advanced options" section for commit, shared folder method, and pytorch options - Can be run in the background - Shows progress in the Downloads tab +- Even more performance improvements for loading and searching the Checkpoints page ### Fixed - Fixed [#97](https://github.com/LykosAI/StabilityMatrix/issues/97) - Codeformer folder should now get linked correctly - Fixed [#106](https://github.com/LykosAI/StabilityMatrix/issues/106) - ComfyUI should now install correctly on Windows machines with an AMD GPU using DirectML -- Fixed [#107](https://github.com/LykosAI/StabilityMatrix/issues/107) - Added `--autolaunch` option to SD.Next +- Fixed [#107](https://github.com/LykosAI/StabilityMatrix/issues/107) - Added `--autolaunch` option to SD.Next +- Fixed [#110](https://github.com/LykosAI/StabilityMatrix/issues/110) - Model Browser should properly navigate to the next page of Installed models +- Installed tag on model browser should now show for connected models imported via drag & drop ## v2.3.4 ### Fixed diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index ef0817e0..3c8a945b 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -177,67 +177,74 @@ public static class DesignData InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0]; InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note."; - // Checkpoints page - CheckpointsPageViewModel.CheckpointFolders = new ObservableCollection - { - new(settingsManager, downloadService, modelFinder) + /*// Checkpoints page + CheckpointsPageViewModel.CheckpointFolders = + new CheckpointFolder[] { - Title = "StableDiffusion", - DirectoryPath = "Models/StableDiffusion", - CheckpointFiles = new AdvancedObservableList + new(settingsManager, downloadService, modelFinder, notificationService) { - new() + Title = "StableDiffusion", + DirectoryPath = "Models/StableDiffusion", + CheckpointFiles = CheckpointFile[] { - 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 + new() { - VersionName = "Lightning Auroral", - BaseModel = "SD 1.5", - ModelName = "Auroral Background", - ModelType = CivitModelType.Model, - FileMetadata = new CivitFileMetadata + 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 { - Format = CivitModelFormat.SafeTensor, - Fp = CivitModelFpType.fp16, - Size = CivitModelSize.pruned, + 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" + }, }, - new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }, }, - }, - new(settingsManager, downloadService, modelFinder) - { - Title = "Lora", - DirectoryPath = "Packages/Lora", - SubFolders = new AdvancedObservableList() + new(settingsManager, downloadService, modelFinder, notificationService) { - new(settingsManager, downloadService, modelFinder) + Title = "Lora", + DirectoryPath = "Packages/Lora", + SubFolders = CheckpointFolder[] { - Title = "StableDiffusion", - DirectoryPath = "Packages/Lora/Subfolder", + new(settingsManager, downloadService, modelFinder, notificationService) + { + Title = "StableDiffusion", + DirectoryPath = "Packages/Lora/Subfolder", + }, + new(settingsManager, downloadService, modelFinder, notificationService) + { + Title = "Lora", + DirectoryPath = "Packages/StableDiffusion/Subfolder", + } }, - new(settingsManager, downloadService, modelFinder) + CheckpointFiles = new AdvancedObservableList { - Title = "Lora", - DirectoryPath = "Packages/StableDiffusion/Subfolder", + new() { FilePath = "~/Models/Lora/lora_v2.pt", Title = "Best Lora v2", } } - }, - CheckpointFiles = new AdvancedObservableList - { - new() { FilePath = "~/Models/Lora/lora_v2.pt", Title = "Best Lora v2", } } - } - }; + }; foreach (var folder in CheckpointsPageViewModel.CheckpointFolders) { - folder.DisplayedCheckpointFiles = folder.CheckpointFiles; - } + folder.DisplayedCheckpointFiles = new AdvancedObservableList( + folder.CheckpointFiles + ); + }*/ CheckpointBrowserViewModel.ModelCards = new ObservableCollection diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 5d65244a..d9fcb10d 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -78,11 +78,29 @@ namespace StabilityMatrix.Avalonia.Languages { } /// - /// Looks up a localized string similar to Connect. + /// Looks up a localized string similar to Continue. /// - public static string Action_Connect { + public static string Action_Continue { get { - return ResourceManager.GetString("Action_Connect", resourceCulture); + return ResourceManager.GetString("Action_Continue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete. + /// + public static string Action_Delete { + get { + return ResourceManager.GetString("Action_Delete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exit Application. + /// + public static string Action_ExitApplication { + get { + return ResourceManager.GetString("Action_ExitApplication", resourceCulture); } } @@ -95,6 +113,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Install. + /// + public static string Action_Install { + get { + return ResourceManager.GetString("Action_Install", resourceCulture); + } + } + /// /// Looks up a localized string similar to Launch. /// @@ -104,6 +131,24 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to New. + /// + public static string Action_New { + get { + return ResourceManager.GetString("Action_New", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open on CivitAI. + /// + public static string Action_OpenOnCivitAi { + get { + return ResourceManager.GetString("Action_OpenOnCivitAi", resourceCulture); + } + } + /// /// Looks up a localized string similar to Quit. /// @@ -131,6 +176,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Rename. + /// + public static string Action_Rename { + get { + return ResourceManager.GetString("Action_Rename", resourceCulture); + } + } + /// /// Looks up a localized string similar to Save. /// @@ -140,6 +194,60 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Search. + /// + public static string Action_Search { + get { + return ResourceManager.GetString("Action_Search", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show in Explorer. + /// + public static string Action_ShowInExplorer { + get { + return ResourceManager.GetString("Action_ShowInExplorer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Advanced Options. + /// + public static string Label_AdvancedOptions { + get { + return ResourceManager.GetString("Label_AdvancedOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All Versions. + /// + public static string Label_AllVersions { + get { + return ResourceManager.GetString("Label_AllVersions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Base Model. + /// + public static string Label_BaseModel { + get { + return ResourceManager.GetString("Label_BaseModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Become a Patron. + /// + public static string Label_BecomeAPatron { + get { + return ResourceManager.GetString("Label_BecomeAPatron", resourceCulture); + } + } + /// /// Looks up a localized string similar to Branches. /// @@ -150,47 +258,83 @@ namespace StabilityMatrix.Avalonia.Languages { } /// - /// Looks up a localized string similar to CFG Scale. + /// Looks up a localized string similar to Categories. /// - public static string Label_CFGScale { + public static string Label_Categories { get { - return ResourceManager.GetString("Label_CFGScale", resourceCulture); + return ResourceManager.GetString("Label_Categories", resourceCulture); } } /// - /// Looks up a localized string similar to Comments. + /// Looks up a localized string similar to Close dialog when finished. /// - public static string Label_Comments { + public static string Label_CloseDialogWhenFinished { get { - return ResourceManager.GetString("Label_Comments", resourceCulture); + return ResourceManager.GetString("Label_CloseDialogWhenFinished", resourceCulture); } } /// - /// Looks up a localized string similar to Connecting.... + /// Looks up a localized string similar to Commit. /// - public static string Label_ConnectingEllipsis { + public static string Label_Commit { get { - return ResourceManager.GetString("Label_ConnectingEllipsis", resourceCulture); + return ResourceManager.GetString("Label_Commit", resourceCulture); } } /// - /// Looks up a localized string similar to Deemphasis. + /// Looks up a localized string similar to Connected Model. /// - public static string Label_Deemphasis { + public static string Label_ConnectedModel { get { - return ResourceManager.GetString("Label_Deemphasis", resourceCulture); + return ResourceManager.GetString("Label_ConnectedModel", resourceCulture); } } /// - /// Looks up a localized string similar to Denoising Strength. + /// Looks up a localized string similar to Data Directory. /// - public static string Label_DenoisingStrength { + public static string Label_DataDirectory { get { - return ResourceManager.GetString("Label_DenoisingStrength", resourceCulture); + return ResourceManager.GetString("Label_DataDirectory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is where model checkpoints, LORAs, web UIs, settings, etc. will be installed.. + /// + public static string Label_DataDirectoryExplanation { + get { + return ResourceManager.GetString("Label_DataDirectoryExplanation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data provided by CivitAI. + /// + public static string Label_DataProvidedByCivitAi { + get { + return ResourceManager.GetString("Label_DataProvidedByCivitAi", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display Name. + /// + public static string Label_DisplayName { + get { + return ResourceManager.GetString("Label_DisplayName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Downloads. + /// + public static string Label_Downloads { + get { + return ResourceManager.GetString("Label_Downloads", resourceCulture); } } @@ -204,29 +348,101 @@ namespace StabilityMatrix.Avalonia.Languages { } /// - /// Looks up a localized string similar to Emebeddings / Textual Inversion. + /// Looks up a localized string similar to Drop file here to import. + /// + public static string Label_DropFileToImport { + get { + return ResourceManager.GetString("Label_DropFileToImport", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience.. + /// + public static string Label_FatWarning { + get { + return ResourceManager.GetString("Label_FatWarning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Find Connected Metadata. + /// + public static string Label_FindConnectedMetadata { + get { + return ResourceManager.GetString("Label_FindConnectedMetadata", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to First Page. + /// + public static string Label_FirstPage { + get { + return ResourceManager.GetString("Label_FirstPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Folder. + /// + public static string Label_Folder { + get { + return ResourceManager.GetString("Label_Folder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Import as Connected. + /// + public static string Label_ImportAsConnected { + get { + return ResourceManager.GetString("Label_ImportAsConnected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search for connected metadata on new local imports. /// - public static string Label_EmbeddingsOrTextualInversion { + public static string Label_ImportAsConnectedExplanation { get { - return ResourceManager.GetString("Label_EmbeddingsOrTextualInversion", resourceCulture); + return ResourceManager.GetString("Label_ImportAsConnectedExplanation", resourceCulture); } } /// - /// Looks up a localized string similar to Emphasis. + /// Looks up a localized string similar to Import Latest -. /// - public static string Label_Emphasis { + public static string Label_ImportLatest { get { - return ResourceManager.GetString("Label_Emphasis", resourceCulture); + return ResourceManager.GetString("Label_ImportLatest", resourceCulture); } } /// - /// Looks up a localized string similar to Height. + /// Looks up a localized string similar to Indexing.... /// - public static string Label_Height { + public static string Label_Indexing { get { - return ResourceManager.GetString("Label_Height", resourceCulture); + return ResourceManager.GetString("Label_Indexing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An installation with this name already exists.. + /// + public static string Label_InstallationWithThisNameExists { + get { + return ResourceManager.GetString("Label_InstallationWithThisNameExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Join Discord Server. + /// + public static string Label_JoinDiscord { + get { + return ResourceManager.GetString("Label_JoinDiscord", resourceCulture); } } @@ -240,20 +456,101 @@ namespace StabilityMatrix.Avalonia.Languages { } /// - /// Looks up a localized string similar to Model. + /// Looks up a localized string similar to Last Page. + /// + public static string Label_LastPage { + get { + return ResourceManager.GetString("Label_LastPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Let's get started. + /// + public static string Label_LetsGetStarted { + get { + return ResourceManager.GetString("Label_LetsGetStarted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to License Agreement.. + /// + public static string Label_LicenseAgreement { + get { + return ResourceManager.GetString("Label_LicenseAgreement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Local Model. + /// + public static string Label_LocalModel { + get { + return ResourceManager.GetString("Label_LocalModel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Model Description. + /// + public static string Label_ModelDescription { + get { + return ResourceManager.GetString("Label_ModelDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search models, #tags, or @users. + /// + public static string Label_ModelSearchWatermark { + get { + return ResourceManager.GetString("Label_ModelSearchWatermark", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Models Folder. + /// + public static string Label_ModelsFolder { + get { + return ResourceManager.GetString("Label_ModelsFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Model Type. + /// + public static string Label_ModelType { + get { + return ResourceManager.GetString("Label_ModelType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A new version of Stability Matrix is available!. + /// + public static string Label_NewVersionAvailable { + get { + return ResourceManager.GetString("Label_NewVersionAvailable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Next Image. /// - public static string Label_Model { + public static string Label_NextImage { get { - return ResourceManager.GetString("Label_Model", resourceCulture); + return ResourceManager.GetString("Label_NextImage", resourceCulture); } } /// - /// Looks up a localized string similar to Networks (Lora / LyCORIS). + /// Looks up a localized string similar to Next Page. /// - public static string Label_NetworksLoraOrLycoris { + public static string Label_NextPage { get { - return ResourceManager.GetString("Label_NetworksLoraOrLycoris", resourceCulture); + return ResourceManager.GetString("Label_NextPage", resourceCulture); } } @@ -267,11 +564,74 @@ namespace StabilityMatrix.Avalonia.Languages { } /// - /// Looks up a localized string similar to Refiner. + /// Looks up a localized string similar to Page. /// - public static string Label_Refiner { + public static string Label_Page { get { - return ResourceManager.GetString("Label_Refiner", resourceCulture); + return ResourceManager.GetString("Label_Page", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please choose a different name or select a different install location.. + /// + public static string Label_PleaseChooseDifferentName { + get { + return ResourceManager.GetString("Label_PleaseChooseDifferentName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Portable Mode. + /// + public static string Label_PortableMode { + get { + return ResourceManager.GetString("Label_PortableMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In Portable Mode, all data and settings will be stored in the same directory as the application. You will be able to move the application with its 'Data' folder to a different location or computer.. + /// + public static string Label_PortableModeExplanation { + get { + return ResourceManager.GetString("Label_PortableModeExplanation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Previous Image. + /// + public static string Label_PreviousImage { + get { + return ResourceManager.GetString("Label_PreviousImage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Previous Page. + /// + public static string Label_PreviousPage { + get { + return ResourceManager.GetString("Label_PreviousPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PyTorch Version. + /// + public static string Label_PyTorchVersion { + get { + return ResourceManager.GetString("Label_PyTorchVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to I have read and agree to the. + /// + public static string Label_ReadAndAgree { + get { + return ResourceManager.GetString("Label_ReadAndAgree", resourceCulture); } } @@ -294,92 +654,101 @@ namespace StabilityMatrix.Avalonia.Languages { } /// - /// Looks up a localized string similar to Show pixel grid at high zoom levels. + /// Looks up a localized string similar to Shared Model Folder Strategy. /// - public static string Label_ShowPixelGridAtHighZoomLevels { + public static string Label_SharedModelFolderStrategy { get { - return ResourceManager.GetString("Label_ShowPixelGridAtHighZoomLevels", resourceCulture); + return ResourceManager.GetString("Label_SharedModelFolderStrategy", resourceCulture); } } /// - /// Looks up a localized string similar to Steps. + /// Looks up a localized string similar to Show Model Images. /// - public static string Label_Steps { + public static string Label_ShowModelImages { get { - return ResourceManager.GetString("Label_Steps", resourceCulture); + return ResourceManager.GetString("Label_ShowModelImages", resourceCulture); } } /// - /// Looks up a localized string similar to Steps - Base. + /// Looks up a localized string similar to Show NSFW Content. /// - public static string Label_StepsBase { + public static string Label_ShowNsfwContent { get { - return ResourceManager.GetString("Label_StepsBase", resourceCulture); + return ResourceManager.GetString("Label_ShowNsfwContent", resourceCulture); } } /// - /// Looks up a localized string similar to Steps - Refiner. + /// Looks up a localized string similar to Skip first-time setup. /// - public static string Label_StepsRefiner { + public static string Label_SkipSetup { get { - return ResourceManager.GetString("Label_StepsRefiner", resourceCulture); + return ResourceManager.GetString("Label_SkipSetup", resourceCulture); } } /// - /// Looks up a localized string similar to Unknown Package. + /// Looks up a localized string similar to Sort. /// - public static string Label_UnknownPackage { + public static string Label_Sort { get { - return ResourceManager.GetString("Label_UnknownPackage", resourceCulture); + return ResourceManager.GetString("Label_Sort", resourceCulture); } } /// - /// Looks up a localized string similar to VAE. + /// Looks up a localized string similar to Period. /// - public static string Label_VAE { + public static string Label_TimePeriod { get { - return ResourceManager.GetString("Label_VAE", resourceCulture); + return ResourceManager.GetString("Label_TimePeriod", resourceCulture); } } /// - /// Looks up a localized string similar to Version. + /// Looks up a localized string similar to An unexpected error occurred. /// - public static string Label_Version { + public static string Label_UnexpectedErrorOccurred { get { - return ResourceManager.GetString("Label_Version", resourceCulture); + return ResourceManager.GetString("Label_UnexpectedErrorOccurred", resourceCulture); } } /// - /// Looks up a localized string similar to Version Type. + /// Looks up a localized string similar to Unknown Package. /// - public static string Label_VersionType { + public static string Label_UnknownPackage { get { - return ResourceManager.GetString("Label_VersionType", resourceCulture); + return ResourceManager.GetString("Label_UnknownPackage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Update Available. + /// + public static string Label_UpdateAvailable { + get { + return ResourceManager.GetString("Label_UpdateAvailable", resourceCulture); } } /// - /// Looks up a localized string similar to Waiting to connect.... + /// Looks up a localized string similar to Version. /// - public static string Label_WaitingToConnectEllipsis { + public static string Label_Version { get { - return ResourceManager.GetString("Label_WaitingToConnectEllipsis", resourceCulture); + return ResourceManager.GetString("Label_Version", resourceCulture); } } /// - /// Looks up a localized string similar to Width. + /// Looks up a localized string similar to Version Type. /// - public static string Label_Width { + public static string Label_VersionType { get { - return ResourceManager.GetString("Label_Width", resourceCulture); + return ResourceManager.GetString("Label_VersionType", resourceCulture); } } diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx index ccd583bf..4fcc66bd 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx @@ -20,4 +20,37 @@ 言語 + + アプリケーションを終了する + + + インポート + + + インストール + + + 打ち上げる + + + 再起動する + + + やめる + + + 後で再起動する + + + パトロンになる + + + ブランチ + + + 期間 + + + 注文 + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 40e47cbc..af9a85a4 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -69,64 +69,187 @@ Drag & Drop checkpoints here to import - - Emphasis + + Update Available - - Deemphasis + + Become a Patron - - Emebeddings / Textual Inversion + + Join Discord Server - - Networks (Lora / LyCORIS) + + Downloads - - Comments + + Install - - Show pixel grid at high zoom levels + + Skip first-time setup - - Steps + + An unexpected error occurred - - Steps - Base + + Exit Application - - Steps - Refiner + + Display Name - - CFG Scale + + An installation with this name already exists. - - Denoising Strength + + Please choose a different name or select a different install location. - - Width + + Advanced Options - - Height + + Commit - - Refiner + + Shared Model Folder Strategy - - VAE + + PyTorch Version - - Model - - - Connect - - - Connecting... + + Close dialog when finished Close - - Waiting to connect... + + Data Directory + + + This is where model checkpoints, LORAs, web UIs, settings, etc. will be installed. + + + You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience. + + + Portable Mode + + + In Portable Mode, all data and settings will be stored in the same directory as the application. You will be able to move the application with its 'Data' folder to a different location or computer. + + + Continue + + + Previous Image + + + Next Image + + + Model Description + + + A new version of Stability Matrix is available! + + + Import Latest - + + + All Versions + + + Search models, #tags, or @users + + + Search + + + Sort + + + Period + + + Model Type + + + Base Model + + + Show NSFW Content + + + Data provided by CivitAI + + + Page + + + First Page + + + Previous Page + + + Next Page + + + Last Page + + + Rename + + + Delete + + + Open on CivitAI + + + Connected Model + + + Local Model + + + Show in Explorer + + + New + + + Folder + + + Drop file here to import + + + Import as Connected + + + Search for connected metadata on new local imports + + + Indexing... + + + Models Folder + + + Categories + + + Let's get started + + + I have read and agree to the + + + License Agreement. + + + Find Connected Metadata + + + Show Model Images diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs index 84e059f3..f1260f93 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs @@ -45,14 +45,24 @@ public partial class CheckpointFile : ViewModelBase private ConnectedModelInfo? connectedModel; public bool IsConnectedModel => ConnectedModel != null; - [ObservableProperty] private bool isLoading; - [ObservableProperty] private CivitModelType modelType; - + [ObservableProperty] + private bool isLoading; + + [ObservableProperty] + private CivitModelType modelType; + public string FileName => Path.GetFileName(FilePath); public ObservableCollection Badges { get; set; } = new(); - private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" }; + public static readonly string[] SupportedCheckpointExtensions = + { + ".safetensors", + ".pt", + ".ckpt", + ".pth", + ".bin" + }; private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" }; private static readonly string[] SupportedMetadataExtensions = { ".json" }; @@ -78,10 +88,11 @@ public partial class CheckpointFile : ViewModelBase if (string.IsNullOrEmpty(FilePath)) { throw new InvalidOperationException( - "Cannot get connected model info file path when FilePath is empty"); + "Cannot get connected model info file path when FilePath is empty" + ); } - var modelNameNoExt = Path.GetFileNameWithoutExtension((string?) FilePath); - var modelDir = Path.GetDirectoryName((string?) FilePath) ?? ""; + var modelNameNoExt = Path.GetFileNameWithoutExtension((string?)FilePath); + var modelDir = Path.GetDirectoryName((string?)FilePath) ?? ""; return Path.Combine(modelDir, $"{modelNameNoExt}.cm-info.json"); } @@ -125,7 +136,7 @@ public partial class CheckpointFile : ViewModelBase private async Task RenameAsync() { // Parent folder path - var parentPath = Path.GetDirectoryName((string?) FilePath) ?? ""; + var parentPath = Path.GetDirectoryName((string?)FilePath) ?? ""; var textFields = new TextBoxField[] { @@ -134,18 +145,18 @@ public partial class CheckpointFile : ViewModelBase Label = "File name", Validator = text => { - if (string.IsNullOrWhiteSpace(text)) throw new - DataValidationException("File name is required"); - - if (File.Exists(Path.Combine(parentPath, text))) throw new - DataValidationException("File name already exists"); + if (string.IsNullOrWhiteSpace(text)) + throw new DataValidationException("File name is required"); + + if (File.Exists(Path.Combine(parentPath, text))) + throw new DataValidationException("File name already exists"); }, Text = FileName } }; var dialog = DialogHelper.CreateTextEntryDialog("Rename Model", "", textFields); - + if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var name = textFields[0].Text; @@ -160,7 +171,10 @@ public partial class CheckpointFile : ViewModelBase // If preview image exists, rename it too if (PreviewImagePath != null && File.Exists(PreviewImagePath)) { - var newPreviewImagePath = Path.Combine(parentPath, $"{nameNoExt}.preview{Path.GetExtension((string?) PreviewImagePath)}"); + var newPreviewImagePath = Path.Combine( + parentPath, + $"{nameNoExt}.preview{Path.GetExtension((string?)PreviewImagePath)}" + ); File.Move(PreviewImagePath, newPreviewImagePath); PreviewImagePath = newPreviewImagePath; } @@ -170,7 +184,10 @@ public partial class CheckpointFile : ViewModelBase var cmInfoPath = Path.Combine(parentPath, $"{originalNameNoExt}.cm-info.json"); if (File.Exists(cmInfoPath)) { - File.Move(cmInfoPath, Path.Combine(parentPath, $"{nameNoExt}.cm-info.json")); + File.Move( + cmInfoPath, + Path.Combine(parentPath, $"{nameNoExt}.cm-info.json") + ); } } } @@ -184,7 +201,8 @@ public partial class CheckpointFile : ViewModelBase [RelayCommand] private void OpenOnCivitAi() { - if (ConnectedModel?.ModelId == null) return; + if (ConnectedModel?.ModelId == null) + return; ProcessRunner.OpenUrl($"https://civitai.com/models/{ConnectedModel.ModelId}"); } @@ -195,22 +213,33 @@ public partial class CheckpointFile : ViewModelBase /// - {filename}.preview.{image-extensions} (preview image) /// - {filename}.cm-info.json (connected model info) /// - public static IEnumerable FromDirectoryIndex(string directory, SearchOption searchOption = SearchOption.TopDirectoryOnly) + public static IEnumerable FromDirectoryIndex( + string directory, + SearchOption searchOption = SearchOption.TopDirectoryOnly + ) { foreach (var file in Directory.EnumerateFiles(directory, "*.*", searchOption)) { - if (!SupportedCheckpointExtensions.Any(ext => - Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase))) + if ( + !SupportedCheckpointExtensions.Any( + ext => + Path.GetExtension(file) + .Equals(ext, StringComparison.InvariantCultureIgnoreCase) + ) + ) continue; - + var checkpointFile = new CheckpointFile { Title = Path.GetFileNameWithoutExtension(file), FilePath = Path.Combine(directory, file), }; - - var jsonPath = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.cm-info.json"); - if (File.Exists(jsonPath)) + + var jsonPath = Path.Combine( + directory, + $"{Path.GetFileNameWithoutExtension(file)}.cm-info.json" + ); + if (File.Exists(jsonPath)) { var json = File.ReadAllText(jsonPath); var connectedModelInfo = ConnectedModelInfo.FromJson(json); @@ -218,32 +247,51 @@ public partial class CheckpointFile : ViewModelBase } checkpointFile.PreviewImagePath = SupportedImageExtensions - .Select(ext => Path.Combine(directory, - $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists) + .Select( + ext => + Path.Combine( + directory, + $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}" + ) + ) + .Where(File.Exists) .FirstOrDefault(); yield return checkpointFile; } } - + public static IEnumerable GetAllCheckpointFiles(string modelsDirectory) { - foreach (var file in Directory.EnumerateFiles(modelsDirectory, "*.*", SearchOption.AllDirectories)) + foreach ( + var file in Directory.EnumerateFiles( + modelsDirectory, + "*.*", + SearchOption.AllDirectories + ) + ) { - if (!SupportedCheckpointExtensions.Any(ext => - Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase))) + if ( + !SupportedCheckpointExtensions.Any( + ext => + Path.GetExtension(file) + .Equals(ext, StringComparison.InvariantCultureIgnoreCase) + ) + ) continue; - + var checkpointFile = new CheckpointFile { Title = Path.GetFileNameWithoutExtension(file), FilePath = file, }; - var jsonPath = Path.Combine(Path.GetDirectoryName(file) ?? "", - Path.GetFileNameWithoutExtension(file) + ".cm-info.json"); - - if (File.Exists(jsonPath)) + var jsonPath = Path.Combine( + Path.GetDirectoryName(file) ?? "", + Path.GetFileNameWithoutExtension(file) + ".cm-info.json" + ); + + if (File.Exists(jsonPath)) { var json = File.ReadAllText(jsonPath); var connectedModelInfo = ConnectedModelInfo.FromJson(json); @@ -252,8 +300,14 @@ public partial class CheckpointFile : ViewModelBase } checkpointFile.PreviewImagePath = SupportedImageExtensions - .Select(ext => Path.Combine(Path.GetDirectoryName(file) ?? "", - $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists) + .Select( + ext => + Path.Combine( + Path.GetDirectoryName(file) ?? "", + $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}" + ) + ) + .Where(File.Exists) .FirstOrDefault(); yield return checkpointFile; @@ -263,8 +317,11 @@ public partial class CheckpointFile : ViewModelBase /// /// Index with progress reporting. /// - public static IEnumerable FromDirectoryIndex(string directory, IProgress progress, - SearchOption searchOption = SearchOption.TopDirectoryOnly) + public static IEnumerable FromDirectoryIndex( + string directory, + IProgress progress, + SearchOption searchOption = SearchOption.TopDirectoryOnly + ) { var current = 0ul; foreach (var checkpointFile in FromDirectoryIndex(directory, searchOption)) @@ -281,7 +338,7 @@ public partial class CheckpointFile : ViewModelBase { return CivitModelType.Checkpoint; } - + if (filePath.Contains(SharedFolderType.ControlNet.ToString())) { return CivitModelType.Controlnet; @@ -309,4 +366,31 @@ public partial class CheckpointFile : ViewModelBase return CivitModelType.Unknown; } + + private sealed class FilePathEqualityComparer : IEqualityComparer + { + public bool Equals(CheckpointFile? x, CheckpointFile? y) + { + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; + return x.FilePath == y.FilePath + && x.ConnectedModel?.Hashes.BLAKE3 == y.ConnectedModel?.Hashes.BLAKE3 + && x.ConnectedModel?.ThumbnailImageUrl == y.ConnectedModel?.ThumbnailImageUrl + && x.PreviewImagePath == y.PreviewImagePath; + } + + public int GetHashCode(CheckpointFile obj) + { + return obj.FilePath.GetHashCode(); + } + } + + public static IEqualityComparer FilePathComparer { get; } = + new FilePathEqualityComparer(); } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs index c7d51c5e..f245dd66 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs @@ -5,13 +5,16 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DynamicData; +using DynamicData.Binding; using FluentAvalonia.UI.Controls; -using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; @@ -28,9 +31,17 @@ public partial class CheckpointFolder : ViewModelBase private readonly ISettingsManager settingsManager; private readonly IDownloadService downloadService; private readonly ModelFinder modelFinder; + private readonly INotificationService notificationService; + + public SourceCache SubFoldersCache { get; } = + new(x => x.DirectoryPath); + + private readonly SourceCache checkpointFilesCache = + new(x => x.FilePath); + // ReSharper disable once FieldCanBeMadeReadOnly.Local private bool useCategoryVisibility; - + /// /// Absolute path to the folder. /// @@ -50,12 +61,14 @@ public partial class CheckpointFolder : ViewModelBase /// /// True if the category is enabled for the manager page. /// - [ObservableProperty] private bool isCategoryEnabled = true; + [ObservableProperty] + private bool isCategoryEnabled = true; /// /// True if currently expanded in the UI. /// - [ObservableProperty] private bool isExpanded = true; + [ObservableProperty] + private bool isExpanded = true; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))] @@ -65,64 +78,97 @@ public partial class CheckpointFolder : ViewModelBase [NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))] private bool isImportInProgress; - [ObservableProperty] private string searchFilter = string.Empty; - + [ObservableProperty] + private string searchFilter = string.Empty; + public bool IsDragBlurEnabled => IsCurrentDragTarget || IsImportInProgress; public string TitleWithFilesCount => CheckpointFiles.Any() || SubFolders.Any(f => f.CheckpointFiles.Any()) ? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})" : Title; - + public ProgressViewModel Progress { get; } = new(); public CheckpointFolder? ParentFolder { get; init; } - public AdvancedObservableList SubFolders { get; init; } = new(); - public AdvancedObservableList CheckpointFiles { get; init; } = new(); - public AdvancedObservableList DisplayedCheckpointFiles { get; set; } + + public IObservableCollection SubFolders { get; } = + new ObservableCollectionExtended(); + + public IObservableCollection CheckpointFiles { get; } = + new ObservableCollectionExtended(); + + public IObservableCollection DisplayedCheckpointFiles { get; } = + new ObservableCollectionExtended(); public CheckpointFolder( ISettingsManager settingsManager, IDownloadService downloadService, ModelFinder modelFinder, - bool useCategoryVisibility = true) + INotificationService notificationService, + bool useCategoryVisibility = true + ) { this.settingsManager = settingsManager; this.downloadService = downloadService; this.modelFinder = modelFinder; + this.notificationService = notificationService; this.useCategoryVisibility = useCategoryVisibility; - + + checkpointFilesCache + .Connect() + .DeferUntilLoaded() + .Bind(CheckpointFiles) + .Sort( + SortExpressionComparer + .Descending(f => f.IsConnectedModel) + .ThenByAscending( + f => f.IsConnectedModel ? f.ConnectedModel!.ModelName : f.FileName + ) + ) + .Filter( + f => + f.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase) + || f.Title.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase) + ) + .Bind(DisplayedCheckpointFiles) + .Subscribe(); + + SubFoldersCache + .Connect() + .DeferUntilLoaded() + .SortBy(x => x.Title) + .Bind(SubFolders) + .Subscribe(); + CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged; - DisplayedCheckpointFiles = CheckpointFiles; + // DisplayedCheckpointFiles = CheckpointFiles; } - + /// /// When title is set, set the category enabled state from settings. /// // ReSharper disable once UnusedParameterInPartialMethod partial void OnTitleChanged(string value) { - if (!useCategoryVisibility) return; - + if (!useCategoryVisibility) + return; + // Update folder type var result = Enum.TryParse(Title, out SharedFolderType type); FolderType = result ? type : new SharedFolderType(); - + IsCategoryEnabled = settingsManager.IsSharedFolderCategoryVisible(FolderType); } partial void OnSearchFilterChanged(string value) { - if (string.IsNullOrEmpty(value)) - { - DisplayedCheckpointFiles = CheckpointFiles; - } - else + foreach (var subFolder in SubFolders) { - var filteredFiles = CheckpointFiles.Where(y => - y.FileName.Contains(value, StringComparison.OrdinalIgnoreCase)); - DisplayedCheckpointFiles = new AdvancedObservableList(filteredFiles); + subFolder.SearchFilter = value; } + + checkpointFilesCache.Refresh(); } /// @@ -130,13 +176,14 @@ public partial class CheckpointFolder : ViewModelBase /// partial void OnIsCategoryEnabledChanged(bool value) { - if (!useCategoryVisibility) return; + if (!useCategoryVisibility) + return; if (value != settingsManager.IsSharedFolderCategoryVisible(FolderType)) { settingsManager.SetSharedFolderCategoryVisible(FolderType, value); } } - + private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(TitleWithFilesCount)); @@ -168,7 +215,7 @@ public partial class CheckpointFolder : ViewModelBase { await ProcessRunner.OpenFolderBrowser(path); } - + [RelayCommand] private async Task Delete() { @@ -181,7 +228,9 @@ public partial class CheckpointFolder : ViewModelBase } var dialog = DialogHelper.CreateTaskDialog( - "Are you sure you want to delete this folder?",directory); + "Are you sure you want to delete this folder?", + directory + ); dialog.ShowProgressBar = false; dialog.Buttons = new List @@ -189,7 +238,7 @@ public partial class CheckpointFolder : ViewModelBase TaskDialogButton.YesButton, TaskDialogButton.NoButton }; - + dialog.Closing += async (sender, e) => { // We only want to use the deferral on the 'Yes' Button @@ -204,7 +253,7 @@ public partial class CheckpointFolder : ViewModelBase { await directory.DeleteAsync(true); } - + RemoveFromParentList(); deferral.Complete(); } @@ -214,12 +263,12 @@ public partial class CheckpointFolder : ViewModelBase await dialog.ShowAsync(true); } - + [RelayCommand] private async Task CreateSubFolder() { Dispatcher.UIThread.VerifyAccess(); - + var textBox = new TextBox(); var dialog = new ContentDialog { @@ -235,52 +284,71 @@ public partial class CheckpointFolder : ViewModelBase if (result == ContentDialogResult.Primary) { var targetName = textBox.Text; - if (string.IsNullOrWhiteSpace(targetName)) return; - + if (string.IsNullOrWhiteSpace(targetName)) + return; + var subFolderPath = Path.Combine(DirectoryPath, targetName); - + Directory.CreateDirectory(subFolderPath); - - SubFolders.Add(new CheckpointFolder(settingsManager, - downloadService, modelFinder, - useCategoryVisibility: false) - { - Title = Path.GetFileName(subFolderPath), - DirectoryPath = subFolderPath, - FolderType = FolderType, - ParentFolder = this, - IsExpanded = false, - }); + + SubFolders.Add( + new CheckpointFolder( + settingsManager, + downloadService, + modelFinder, + notificationService, + useCategoryVisibility: false + ) + { + Title = Path.GetFileName(subFolderPath), + DirectoryPath = subFolderPath, + FolderType = FolderType, + ParentFolder = this, + IsExpanded = false, + } + ); } } - + /// /// Imports files to the folder. Reports progress to instance properties. /// - public async Task ImportFilesAsync(IEnumerable files, bool convertToConnected = false) + public async Task ImportFilesAsync( + IEnumerable files, + bool convertToConnected = false, + bool copyFiles = true + ) { try { Progress.Value = 0; - var copyPaths = files.ToDictionary(k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v))); - + var copyPaths = files.ToDictionary( + k => k, + v => Path.Combine(DirectoryPath, Path.GetFileName(v)) + ); + var progress = new Progress(report => { Progress.IsIndeterminate = false; Progress.Value = report.Percentage; // For multiple files, add count - Progress.Text = copyPaths.Count > 1 ? $"Importing {report.Title} ({report.Message})" : $"Importing {report.Title}"; + Progress.Text = + copyPaths.Count > 1 + ? $"Importing {report.Title} ({report.Message})" + : $"Importing {report.Title}"; }); - await FileTransfers.CopyFiles(copyPaths, progress); - + if (copyFiles) + { + await FileTransfers.CopyFiles(copyPaths, progress); + } + // Hash files and convert them to connected model if found if (convertToConnected) { var modelFilesCount = copyPaths.Count; - var modelFiles = copyPaths.Values - .Select(path => new FilePath(path)); - + var modelFiles = copyPaths.Values.Select(path => new FilePath(path)); + // Holds tasks for model queries after hash var modelQueryTasks = new List>(); @@ -290,27 +358,33 @@ public partial class CheckpointFolder : ViewModelBase { Progress.IsIndeterminate = report.IsIndeterminate; Progress.Value = report.Percentage; - Progress.Text = modelFilesCount > 1 ? - $"Computing metadata for {modelFile.Name} ({i}/{modelFilesCount})" : - $"Computing metadata for {modelFile.Name}"; + Progress.Text = + modelFilesCount > 1 + ? $"Computing metadata for {modelFile.Name} ({i}/{modelFilesCount})" + : $"Computing metadata for {modelFile.Name}"; }); - + var hashBlake3 = await FileHash.GetBlake3Async(modelFile, hashProgress); - + // Start a task to query the model in background var queryTask = Task.Run(async () => { var result = await modelFinder.LocalFindModel(hashBlake3); result ??= await modelFinder.RemoteFindModel(hashBlake3); - if (result is null) return false; // Not found + if (result is null) + return false; // Not found var (model, version, file) = result.Value; - + // Save connected model info json var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name); var modelInfo = new ConnectedModelInfo( - model, version, file, DateTimeOffset.UtcNow); + model, + version, + file, + DateTimeOffset.UtcNow + ); await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName); // If available, save thumbnail @@ -321,8 +395,15 @@ public partial class CheckpointFolder : ViewModelBase if (imageExt is "jpg" or "jpeg" or "png") { var imageDownloadPath = Path.GetFullPath( - Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}")); - await downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); + Path.Combine( + DirectoryPath, + $"{modelFileName}.preview.{imageExt}" + ) + ); + await downloadService.DownloadToFileAsync( + image.Url, + imageDownloadPath + ); } } @@ -330,29 +411,27 @@ public partial class CheckpointFolder : ViewModelBase }); modelQueryTasks.Add(queryTask); } - + // Set progress to indeterminate Progress.IsIndeterminate = true; Progress.Text = "Checking connected model information"; - + // Wait for all model queries to finish var modelQueryResults = await Task.WhenAll(modelQueryTasks); - + var successCount = modelQueryResults.Count(r => r); var totalCount = modelQueryResults.Length; var failCount = totalCount - successCount; - await IndexAsync(); - + BackgroundIndex(); + Progress.Value = 100; Progress.Text = successCount switch { - 0 when failCount > 0 => - "Import complete. No connected data found.", - > 0 when failCount > 0 => - $"Import complete. Found connected data for {successCount} of {totalCount} models.", - 1 when failCount == 0 => - "Import complete. Found connected data for 1 model.", + 0 when failCount > 0 => "Import complete. No connected data found.", + > 0 when failCount > 0 + => $"Import complete. Found connected data for {successCount} of {totalCount} models.", + 1 when failCount == 0 => "Import complete. Found connected data for 1 model.", _ => $"Import complete. Found connected data for all {totalCount} models." }; } @@ -360,7 +439,7 @@ public partial class CheckpointFolder : ViewModelBase { Progress.Text = "Import complete"; Progress.Value = 100; - await IndexAsync(); + BackgroundIndex(); } } finally @@ -369,65 +448,107 @@ public partial class CheckpointFolder : ViewModelBase } } + public async Task FindConnectedMetadata() + { + IsImportInProgress = true; + var files = CheckpointFiles + .Where(f => !f.IsConnectedModel) + .Select(f => f.FilePath) + .ToList(); + + if (files.Any()) + { + await ImportFilesAsync(files, true, false); + } + else + { + notificationService.Show( + "Cannot Find Connected Metadata", + "All files in this folder are already connected.", + NotificationType.Warning + ); + } + } + /// /// Clears progress after a delay. /// private void DelayedClearProgress(TimeSpan delay) { - Task.Delay(delay).ContinueWith(_ => - { - IsImportInProgress = false; - Progress.Value = 0; - Progress.IsIndeterminate = false; - Progress.Text = string.Empty; - }); + Task.Delay(delay) + .ContinueWith(_ => + { + IsImportInProgress = false; + Progress.Value = 0; + Progress.IsIndeterminate = false; + Progress.Text = string.Empty; + }); } - + /// /// Gets checkpoint files from folder index /// - private async Task> GetCheckpointFilesAsync(IProgress? progress = default) + private IEnumerable GetCheckpointFiles() { if (!Directory.Exists(DirectoryPath)) { - return new List(); + return Enumerable.Empty(); } - return await (progress switch - { - null => Task.Run(() => - CheckpointFile.FromDirectoryIndex(DirectoryPath).ToList()), - - _ => Task.Run(() => - CheckpointFile.FromDirectoryIndex(DirectoryPath, progress).ToList()) - }); + return CheckpointFile.FromDirectoryIndex(DirectoryPath); + } + + /// + /// Indexes the folder for checkpoint files and refreshes the CheckPointFiles collection. + /// + public void BackgroundIndex() + { + Dispatcher.UIThread.Post(Index, DispatcherPriority.Background); } /// /// Indexes the folder for checkpoint files and refreshes the CheckPointFiles collection. /// - public async Task IndexAsync(IProgress? progress = default) + public void Index() { - SubFolders.Clear(); // Get subfolders foreach (var folder in Directory.GetDirectories(DirectoryPath)) { - // Create subfolder - var subFolder = new CheckpointFolder(settingsManager, - downloadService, modelFinder, useCategoryVisibility: false) + // Get from cache or create new + if (SubFoldersCache.Lookup(folder) is { HasValue: true } result) + { + result.Value.BackgroundIndex(); + } + else { - Title = Path.GetFileName(folder), - DirectoryPath = folder, - FolderType = FolderType, // Inherit our folder type - ParentFolder = this, - IsExpanded = false, // Subfolders are collapsed by default - }; - - await subFolder.IndexAsync(progress); - SubFolders.Add(subFolder); + // Create subfolder + var subFolder = new CheckpointFolder( + settingsManager, + downloadService, + modelFinder, + notificationService, + useCategoryVisibility: false + ) + { + Title = Path.GetFileName(folder), + DirectoryPath = folder, + FolderType = FolderType, // Inherit our folder type + ParentFolder = this, + IsExpanded = false, // Subfolders are collapsed by default + }; + subFolder.BackgroundIndex(); + SubFoldersCache.AddOrUpdate(subFolder); + } } - - CheckpointFiles.Clear(); - CheckpointFiles.AddRange(await GetCheckpointFilesAsync()); + + // Index files + Dispatcher.UIThread.Post( + () => + { + var files = GetCheckpointFiles(); + checkpointFilesCache.EditDiff(files, CheckpointFile.FilePathComparer); + }, + DispatcherPriority.Background + ); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs index b3351ebc..e2c1a7ff 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs @@ -1,15 +1,16 @@ using System; -using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DynamicData; +using DynamicData.Binding; using FluentAvalonia.UI.Controls; using NLog; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.Views; @@ -30,170 +31,177 @@ public partial class CheckpointsPageViewModel : PageViewModelBase private readonly ISettingsManager settingsManager; private readonly ModelFinder modelFinder; private readonly IDownloadService downloadService; + private readonly INotificationService notificationService; public override string Title => "Checkpoints"; - public override IconSource IconSource => new SymbolIconSource - {Symbol = Symbol.Notebook, IsFilled = true}; + public override IconSource IconSource => + new SymbolIconSource { Symbol = Symbol.Notebook, IsFilled = true }; // Toggle button for auto hashing new drag-and-dropped files for connected upgrade - [ObservableProperty] private bool isImportAsConnected; - [ObservableProperty] private bool isLoading; - [ObservableProperty] private bool isIndexing; - + [ObservableProperty] + private bool isImportAsConnected; + + [ObservableProperty] + private bool isLoading; + + [ObservableProperty] + private bool isIndexing; + + [ObservableProperty] + private bool showConnectedModelImages; + [ObservableProperty] private string searchFilter = string.Empty; partial void OnIsImportAsConnectedChanged(bool value) { - if (settingsManager.IsLibraryDirSet && - value != settingsManager.Settings.IsImportAsConnected) + if ( + settingsManager.IsLibraryDirSet && value != settingsManager.Settings.IsImportAsConnected + ) { settingsManager.Transaction(s => s.IsImportAsConnected = value); } } - [ObservableProperty] - private ObservableCollection checkpointFolders = new(); + public SourceCache CheckpointFoldersCache { get; } = + new(x => x.DirectoryPath); - [ObservableProperty] - private ObservableCollection displayedCheckpointFolders = new(); + public IObservableCollection CheckpointFolders { get; } = + new ObservableCollectionExtended(); + + public IObservableCollection DisplayedCheckpointFolders { get; } = + new ObservableCollectionExtended(); public CheckpointsPageViewModel( ISharedFolders sharedFolders, ISettingsManager settingsManager, IDownloadService downloadService, - ModelFinder modelFinder) + INotificationService notificationService, + ModelFinder modelFinder + ) { this.sharedFolders = sharedFolders; this.settingsManager = settingsManager; this.downloadService = downloadService; + this.notificationService = notificationService; this.modelFinder = modelFinder; + + CheckpointFoldersCache + .Connect() + .DeferUntilLoaded() + .SortBy(x => x.Title) + .Bind(CheckpointFolders) + .Filter(ContainsSearchFilter) + .Bind(DisplayedCheckpointFolders) + .Subscribe(); } - + public override async Task OnLoadedAsync() { var sw = Stopwatch.StartNew(); - DisplayedCheckpointFolders = CheckpointFolders; + // DisplayedCheckpointFolders = CheckpointFolders; // Set UI states IsImportAsConnected = settingsManager.Settings.IsImportAsConnected; + ShowConnectedModelImages = settingsManager.Settings.ShowConnectedModelImages; // Refresh search filter OnSearchFilterChanged(string.Empty); - Logger.Info($"Loaded {DisplayedCheckpointFolders.Count} checkpoint folders in {sw.ElapsedMilliseconds}ms"); - - if (Design.IsDesignMode) return; + if (Design.IsDesignMode) + return; IsLoading = CheckpointFolders.Count == 0; IsIndexing = CheckpointFolders.Count > 0; - await IndexFolders(); + // GetStuff(); + 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)) + foreach (var folder in CheckpointFolders) { - DisplayedCheckpointFolders = new ObservableCollection( - CheckpointFolders.Select(x => - { - x.SearchFilter = SearchFilter; - return x; - })); - sw.Stop(); - Logger.Info($"OnSearchFilterChanged in {sw.ElapsedMilliseconds}ms"); - return; + folder.SearchFilter = value; } - - sw.Restart(); - - var filteredFolders = CheckpointFolders - .Where(ContainsSearchFilter).ToList(); - foreach (var folder in filteredFolders) + + CheckpointFoldersCache.Refresh(); + } + + partial void OnShowConnectedModelImagesChanged(bool value) + { + if ( + settingsManager.IsLibraryDirSet + && value != settingsManager.Settings.ShowConnectedModelImages + ) { - folder.SearchFilter = SearchFilter; + settingsManager.Transaction(s => s.ShowConnectedModelImages = value); } - sw.Stop(); - Logger.Info($"ContainsSearchFilter in {sw.ElapsedMilliseconds}ms"); - - DisplayedCheckpointFolders = new ObservableCollection(filteredFolders); } - - private bool ContainsSearchFilter(CheckpointManager.CheckpointFolder folder) + + private bool ContainsSearchFilter(CheckpointFolder folder) { if (folder == null) throw new ArgumentNullException(nameof(folder)); + if (string.IsNullOrWhiteSpace(SearchFilter)) + { + return true; + } + // Check files in the current folder - return folder.CheckpointFiles.Any(x => - x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)) || - // If no matching files were found in the current folder, check in all subfolders - folder.SubFolders.Any(subFolder => ContainsSearchFilter(subFolder)); + return folder.CheckpointFiles.Any( + x => x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase) + ) + || + // If no matching files were found in the current folder, check in all subfolders + folder.SubFolders.Any(ContainsSearchFilter); } - private async Task IndexFolders() + private void IndexFolders() { var modelsDirectory = settingsManager.ModelsDirectory; - // Get all folders within the shared folder root - if (string.IsNullOrWhiteSpace(modelsDirectory)) - { - CheckpointFolders.Clear(); - return; - } - + // Setup shared folders in case they're missing sharedFolders.SetupSharedModelFolders(); var folders = Directory.GetDirectories(modelsDirectory); var sw = Stopwatch.StartNew(); - + // Index all folders - var indexTasks = folders.Select(async f => + + foreach (var folder in folders) { - var checkpointFolder = - new CheckpointFolder(settingsManager, downloadService, modelFinder) + // Get from cache or create new + if (CheckpointFoldersCache.Lookup(folder) is { HasValue: true } result) + { + result.Value.Index(); + } + else + { + var checkpointFolder = new CheckpointFolder( + settingsManager, + downloadService, + modelFinder, + notificationService + ) { - Title = Path.GetFileName(f), - DirectoryPath = f, - IsExpanded = true, // Top level folders expanded by default + Title = Path.GetFileName(folder), + DirectoryPath = folder, + IsExpanded = true // Top level folders expanded by default }; - await checkpointFolder.IndexAsync(); - return checkpointFolder; - }).ToList(); + checkpointFolder.Index(); + CheckpointFoldersCache.AddOrUpdate(checkpointFolder); + } + } - 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 - .Where(x => x.CheckpointFiles.Any(y => y.FileName.Contains(SearchFilter))).Select( - f => - { - f.SearchFilter = SearchFilter; - return f; - }); - DisplayedCheckpointFolders = new ObservableCollection(filtered); - } - else - { - DisplayedCheckpointFolders = CheckpointFolders; - } + Logger.Info($"IndexFolders in {sw.Elapsed.TotalMilliseconds:F1}ms"); } [RelayCommand] diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml index 2a4a3541..ec5c557e 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml @@ -9,6 +9,7 @@ xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager" xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser" xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" + xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700" x:DataType="viewModels:CheckpointBrowserViewModel" d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}" @@ -149,7 +150,7 @@ Margin="0,8,0,0"> - + @@ -162,7 +163,7 @@ IsEnabled="{Binding !IsImporting}" Command="{Binding ShowVersionDialogCommand}" CommandParameter="{Binding CivitModel}" - Content="All Versions" /> + Content="{x:Static lang:Resources.Label_AllVersions}" /> @@ -177,7 +178,7 @@ diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index 860f77b3..23f05e7b 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -54,23 +54,23 @@ + Text="{x:Static lang:Resources.Action_Rename}" IconSource="Rename" /> + Text="{x:Static lang:Resources.Action_Delete}" IconSource="Delete" /> - + @@ -119,6 +119,16 @@ TextWrapping="WrapWithOverflow" IsVisible="{Binding IsConnectedModel}" /> + + + - - - - - - - + + + + + @@ -215,21 +221,24 @@ - - - + - + @@ -284,7 +293,7 @@ Effect="{StaticResource TextDropShadowEffect}" FontSize="24" HorizontalAlignment="Center" - Text="Drop file here to import" + Text="{x:Static lang:Resources.Label_DropFileToImport}" VerticalAlignment="Center" IsVisible="{Binding IsCurrentDragTarget}" /> - + - + + + + + + @@ -382,15 +399,15 @@ BorderThickness="4" IsIndeterminate="True" IsVisible="{Binding IsIndexing}"/> - diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml index 80e41576..f3912584 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml @@ -5,6 +5,7 @@ xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" + xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" d:DataContext="{x:Static mocks:DesignData.ExceptionViewModel}" x:DataType="dialogs:ExceptionViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550" @@ -21,7 +22,7 @@ Grid.Row="0" Theme="{DynamicResource SubtitleTextBlockStyle}" Margin="16" - Text="An unexpected error occured" + Text="{x:Static lang:Resources.Label_UnexpectedErrorOccurred}" TextWrapping="Wrap" VerticalAlignment="Top" /> @@ -74,7 +75,7 @@ @@ -164,7 +165,7 @@ @@ -179,14 +180,14 @@ Margin="0,8,0,0" HorizontalAlignment="Center">