Browse Source

Merge branch 'main' into inference

# Conflicts:
#	StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
#	StabilityMatrix.Avalonia/Languages/Resources.resx
#	StabilityMatrix.Core/Models/Settings/Settings.cs
pull/165/head
Ionite 1 year ago
parent
commit
9f3e892e46
No known key found for this signature in database
  1. 7
      CHANGELOG.md
  2. 95
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  3. 501
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  4. 33
      StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx
  5. 203
      StabilityMatrix.Avalonia/Languages/Resources.resx
  6. 164
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  7. 349
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  8. 194
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  9. 31
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  10. 89
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  11. 5
      StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml
  12. 23
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  13. 5
      StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml
  14. 5
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
  15. 13
      StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml
  16. 11
      StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
  17. 3
      StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml
  18. 11
      StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml
  19. 9
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  20. 2
      StabilityMatrix.Core/Models/Settings/Settings.cs
  21. 180
      StabilityMatrix/Models/CheckpointFolder.cs

7
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

95
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<CheckpointFolder>
{
new(settingsManager, downloadService, modelFinder)
/*// Checkpoints page
CheckpointsPageViewModel.CheckpointFolders =
new CheckpointFolder[]
{
Title = "StableDiffusion",
DirectoryPath = "Models/StableDiffusion",
CheckpointFiles = new AdvancedObservableList<CheckpointFile>
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<CheckpointFolder>()
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<CheckpointFile>
{
Title = "Lora",
DirectoryPath = "Packages/StableDiffusion/Subfolder",
new() { FilePath = "~/Models/Lora/lora_v2.pt", Title = "Best Lora v2", }
}
},
CheckpointFiles = new AdvancedObservableList<CheckpointFile>
{
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<CheckpointFile>(
folder.CheckpointFiles
);
}*/
CheckpointBrowserViewModel.ModelCards =
new ObservableCollection<CheckpointBrowserCardViewModel>

501
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -78,11 +78,29 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// Looks up a localized string similar to Connect.
/// Looks up a localized string similar to Continue.
/// </summary>
public static string Action_Connect {
public static string Action_Continue {
get {
return ResourceManager.GetString("Action_Connect", resourceCulture);
return ResourceManager.GetString("Action_Continue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
public static string Action_Delete {
get {
return ResourceManager.GetString("Action_Delete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exit Application.
/// </summary>
public static string Action_ExitApplication {
get {
return ResourceManager.GetString("Action_ExitApplication", resourceCulture);
}
}
@ -95,6 +113,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Install.
/// </summary>
public static string Action_Install {
get {
return ResourceManager.GetString("Action_Install", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch.
/// </summary>
@ -104,6 +131,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to New.
/// </summary>
public static string Action_New {
get {
return ResourceManager.GetString("Action_New", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open on CivitAI.
/// </summary>
public static string Action_OpenOnCivitAi {
get {
return ResourceManager.GetString("Action_OpenOnCivitAi", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Quit.
/// </summary>
@ -131,6 +176,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Rename.
/// </summary>
public static string Action_Rename {
get {
return ResourceManager.GetString("Action_Rename", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save.
/// </summary>
@ -140,6 +194,60 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Search.
/// </summary>
public static string Action_Search {
get {
return ResourceManager.GetString("Action_Search", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show in Explorer.
/// </summary>
public static string Action_ShowInExplorer {
get {
return ResourceManager.GetString("Action_ShowInExplorer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Advanced Options.
/// </summary>
public static string Label_AdvancedOptions {
get {
return ResourceManager.GetString("Label_AdvancedOptions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to All Versions.
/// </summary>
public static string Label_AllVersions {
get {
return ResourceManager.GetString("Label_AllVersions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Base Model.
/// </summary>
public static string Label_BaseModel {
get {
return ResourceManager.GetString("Label_BaseModel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Become a Patron.
/// </summary>
public static string Label_BecomeAPatron {
get {
return ResourceManager.GetString("Label_BecomeAPatron", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Branches.
/// </summary>
@ -150,47 +258,83 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// Looks up a localized string similar to CFG Scale.
/// Looks up a localized string similar to Categories.
/// </summary>
public static string Label_CFGScale {
public static string Label_Categories {
get {
return ResourceManager.GetString("Label_CFGScale", resourceCulture);
return ResourceManager.GetString("Label_Categories", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Comments.
/// Looks up a localized string similar to Close dialog when finished.
/// </summary>
public static string Label_Comments {
public static string Label_CloseDialogWhenFinished {
get {
return ResourceManager.GetString("Label_Comments", resourceCulture);
return ResourceManager.GetString("Label_CloseDialogWhenFinished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connecting....
/// Looks up a localized string similar to Commit.
/// </summary>
public static string Label_ConnectingEllipsis {
public static string Label_Commit {
get {
return ResourceManager.GetString("Label_ConnectingEllipsis", resourceCulture);
return ResourceManager.GetString("Label_Commit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Deemphasis.
/// Looks up a localized string similar to Connected Model.
/// </summary>
public static string Label_Deemphasis {
public static string Label_ConnectedModel {
get {
return ResourceManager.GetString("Label_Deemphasis", resourceCulture);
return ResourceManager.GetString("Label_ConnectedModel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Denoising Strength.
/// Looks up a localized string similar to Data Directory.
/// </summary>
public static string Label_DenoisingStrength {
public static string Label_DataDirectory {
get {
return ResourceManager.GetString("Label_DenoisingStrength", resourceCulture);
return ResourceManager.GetString("Label_DataDirectory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This is where model checkpoints, LORAs, web UIs, settings, etc. will be installed..
/// </summary>
public static string Label_DataDirectoryExplanation {
get {
return ResourceManager.GetString("Label_DataDirectoryExplanation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Data provided by CivitAI.
/// </summary>
public static string Label_DataProvidedByCivitAi {
get {
return ResourceManager.GetString("Label_DataProvidedByCivitAi", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Display Name.
/// </summary>
public static string Label_DisplayName {
get {
return ResourceManager.GetString("Label_DisplayName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Downloads.
/// </summary>
public static string Label_Downloads {
get {
return ResourceManager.GetString("Label_Downloads", resourceCulture);
}
}
@ -204,29 +348,101 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// Looks up a localized string similar to Emebeddings / Textual Inversion.
/// Looks up a localized string similar to Drop file here to import.
/// </summary>
public static string Label_DropFileToImport {
get {
return ResourceManager.GetString("Label_DropFileToImport", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
public static string Label_FatWarning {
get {
return ResourceManager.GetString("Label_FatWarning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Find Connected Metadata.
/// </summary>
public static string Label_FindConnectedMetadata {
get {
return ResourceManager.GetString("Label_FindConnectedMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to First Page.
/// </summary>
public static string Label_FirstPage {
get {
return ResourceManager.GetString("Label_FirstPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Folder.
/// </summary>
public static string Label_Folder {
get {
return ResourceManager.GetString("Label_Folder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import as Connected.
/// </summary>
public static string Label_ImportAsConnected {
get {
return ResourceManager.GetString("Label_ImportAsConnected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search for connected metadata on new local imports.
/// </summary>
public static string Label_EmbeddingsOrTextualInversion {
public static string Label_ImportAsConnectedExplanation {
get {
return ResourceManager.GetString("Label_EmbeddingsOrTextualInversion", resourceCulture);
return ResourceManager.GetString("Label_ImportAsConnectedExplanation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Emphasis.
/// Looks up a localized string similar to Import Latest -.
/// </summary>
public static string Label_Emphasis {
public static string Label_ImportLatest {
get {
return ResourceManager.GetString("Label_Emphasis", resourceCulture);
return ResourceManager.GetString("Label_ImportLatest", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height.
/// Looks up a localized string similar to Indexing....
/// </summary>
public static string Label_Height {
public static string Label_Indexing {
get {
return ResourceManager.GetString("Label_Height", resourceCulture);
return ResourceManager.GetString("Label_Indexing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An installation with this name already exists..
/// </summary>
public static string Label_InstallationWithThisNameExists {
get {
return ResourceManager.GetString("Label_InstallationWithThisNameExists", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Join Discord Server.
/// </summary>
public static string Label_JoinDiscord {
get {
return ResourceManager.GetString("Label_JoinDiscord", resourceCulture);
}
}
@ -240,20 +456,101 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// Looks up a localized string similar to Model.
/// Looks up a localized string similar to Last Page.
/// </summary>
public static string Label_LastPage {
get {
return ResourceManager.GetString("Label_LastPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Let&apos;s get started.
/// </summary>
public static string Label_LetsGetStarted {
get {
return ResourceManager.GetString("Label_LetsGetStarted", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to License Agreement..
/// </summary>
public static string Label_LicenseAgreement {
get {
return ResourceManager.GetString("Label_LicenseAgreement", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Local Model.
/// </summary>
public static string Label_LocalModel {
get {
return ResourceManager.GetString("Label_LocalModel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Model Description.
/// </summary>
public static string Label_ModelDescription {
get {
return ResourceManager.GetString("Label_ModelDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search models, #tags, or @users.
/// </summary>
public static string Label_ModelSearchWatermark {
get {
return ResourceManager.GetString("Label_ModelSearchWatermark", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Models Folder.
/// </summary>
public static string Label_ModelsFolder {
get {
return ResourceManager.GetString("Label_ModelsFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Model Type.
/// </summary>
public static string Label_ModelType {
get {
return ResourceManager.GetString("Label_ModelType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A new version of Stability Matrix is available!.
/// </summary>
public static string Label_NewVersionAvailable {
get {
return ResourceManager.GetString("Label_NewVersionAvailable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Next Image.
/// </summary>
public static string Label_Model {
public static string Label_NextImage {
get {
return ResourceManager.GetString("Label_Model", resourceCulture);
return ResourceManager.GetString("Label_NextImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Networks (Lora / LyCORIS).
/// Looks up a localized string similar to Next Page.
/// </summary>
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 {
}
/// <summary>
/// Looks up a localized string similar to Refiner.
/// Looks up a localized string similar to Page.
/// </summary>
public static string Label_Refiner {
public static string Label_Page {
get {
return ResourceManager.GetString("Label_Refiner", resourceCulture);
return ResourceManager.GetString("Label_Page", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please choose a different name or select a different install location..
/// </summary>
public static string Label_PleaseChooseDifferentName {
get {
return ResourceManager.GetString("Label_PleaseChooseDifferentName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Portable Mode.
/// </summary>
public static string Label_PortableMode {
get {
return ResourceManager.GetString("Label_PortableMode", resourceCulture);
}
}
/// <summary>
/// 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 &apos;Data&apos; folder to a different location or computer..
/// </summary>
public static string Label_PortableModeExplanation {
get {
return ResourceManager.GetString("Label_PortableModeExplanation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Previous Image.
/// </summary>
public static string Label_PreviousImage {
get {
return ResourceManager.GetString("Label_PreviousImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Previous Page.
/// </summary>
public static string Label_PreviousPage {
get {
return ResourceManager.GetString("Label_PreviousPage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PyTorch Version.
/// </summary>
public static string Label_PyTorchVersion {
get {
return ResourceManager.GetString("Label_PyTorchVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to I have read and agree to the.
/// </summary>
public static string Label_ReadAndAgree {
get {
return ResourceManager.GetString("Label_ReadAndAgree", resourceCulture);
}
}
@ -294,92 +654,101 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// 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.
/// </summary>
public static string Label_ShowPixelGridAtHighZoomLevels {
public static string Label_SharedModelFolderStrategy {
get {
return ResourceManager.GetString("Label_ShowPixelGridAtHighZoomLevels", resourceCulture);
return ResourceManager.GetString("Label_SharedModelFolderStrategy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Steps.
/// Looks up a localized string similar to Show Model Images.
/// </summary>
public static string Label_Steps {
public static string Label_ShowModelImages {
get {
return ResourceManager.GetString("Label_Steps", resourceCulture);
return ResourceManager.GetString("Label_ShowModelImages", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Steps - Base.
/// Looks up a localized string similar to Show NSFW Content.
/// </summary>
public static string Label_StepsBase {
public static string Label_ShowNsfwContent {
get {
return ResourceManager.GetString("Label_StepsBase", resourceCulture);
return ResourceManager.GetString("Label_ShowNsfwContent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Steps - Refiner.
/// Looks up a localized string similar to Skip first-time setup.
/// </summary>
public static string Label_StepsRefiner {
public static string Label_SkipSetup {
get {
return ResourceManager.GetString("Label_StepsRefiner", resourceCulture);
return ResourceManager.GetString("Label_SkipSetup", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown Package.
/// Looks up a localized string similar to Sort.
/// </summary>
public static string Label_UnknownPackage {
public static string Label_Sort {
get {
return ResourceManager.GetString("Label_UnknownPackage", resourceCulture);
return ResourceManager.GetString("Label_Sort", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to VAE.
/// Looks up a localized string similar to Period.
/// </summary>
public static string Label_VAE {
public static string Label_TimePeriod {
get {
return ResourceManager.GetString("Label_VAE", resourceCulture);
return ResourceManager.GetString("Label_TimePeriod", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version.
/// Looks up a localized string similar to An unexpected error occurred.
/// </summary>
public static string Label_Version {
public static string Label_UnexpectedErrorOccurred {
get {
return ResourceManager.GetString("Label_Version", resourceCulture);
return ResourceManager.GetString("Label_UnexpectedErrorOccurred", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version Type.
/// Looks up a localized string similar to Unknown Package.
/// </summary>
public static string Label_VersionType {
public static string Label_UnknownPackage {
get {
return ResourceManager.GetString("Label_VersionType", resourceCulture);
return ResourceManager.GetString("Label_UnknownPackage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Update Available.
/// </summary>
public static string Label_UpdateAvailable {
get {
return ResourceManager.GetString("Label_UpdateAvailable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Waiting to connect....
/// Looks up a localized string similar to Version.
/// </summary>
public static string Label_WaitingToConnectEllipsis {
public static string Label_Version {
get {
return ResourceManager.GetString("Label_WaitingToConnectEllipsis", resourceCulture);
return ResourceManager.GetString("Label_Version", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Width.
/// Looks up a localized string similar to Version Type.
/// </summary>
public static string Label_Width {
public static string Label_VersionType {
get {
return ResourceManager.GetString("Label_Width", resourceCulture);
return ResourceManager.GetString("Label_VersionType", resourceCulture);
}
}

33
StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx

@ -20,4 +20,37 @@
<data name="Label_Language" xml:space="preserve">
<value>言語</value>
</data>
<data name="Action_ExitApplication" xml:space="preserve">
<value>アプリケーションを終了する</value>
</data>
<data name="Action_Import" xml:space="preserve">
<value>インポート</value>
</data>
<data name="Action_Install" xml:space="preserve">
<value>インストール</value>
</data>
<data name="Action_Launch" xml:space="preserve">
<value>打ち上げる</value>
</data>
<data name="Action_Relaunch" xml:space="preserve">
<value>再起動する</value>
</data>
<data name="Action_Quit" xml:space="preserve">
<value>やめる</value>
</data>
<data name="Action_RelaunchLater" xml:space="preserve">
<value>後で再起動する</value>
</data>
<data name="Label_BecomeAPatron" xml:space="preserve">
<value>パトロンになる</value>
</data>
<data name="Label_Branches" xml:space="preserve">
<value>ブランチ</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>期間</value>
</data>
<data name="Label_Sort" xml:space="preserve">
<value>注文</value>
</data>
</root>

203
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -69,64 +69,187 @@
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve">
<value>Drag &amp; Drop checkpoints here to import</value>
</data>
<data name="Label_Emphasis" xml:space="preserve">
<value>Emphasis</value>
<data name="Label_UpdateAvailable" xml:space="preserve">
<value>Update Available</value>
</data>
<data name="Label_Deemphasis" xml:space="preserve">
<value>Deemphasis</value>
<data name="Label_BecomeAPatron" xml:space="preserve">
<value>Become a Patron</value>
</data>
<data name="Label_EmbeddingsOrTextualInversion" xml:space="preserve">
<value>Emebeddings / Textual Inversion</value>
<data name="Label_JoinDiscord" xml:space="preserve">
<value>Join Discord Server</value>
</data>
<data name="Label_NetworksLoraOrLycoris" xml:space="preserve">
<value>Networks (Lora / LyCORIS)</value>
<data name="Label_Downloads" xml:space="preserve">
<value>Downloads</value>
</data>
<data name="Label_Comments" xml:space="preserve">
<value>Comments</value>
<data name="Action_Install" xml:space="preserve">
<value>Install</value>
</data>
<data name="Label_ShowPixelGridAtHighZoomLevels" xml:space="preserve">
<value>Show pixel grid at high zoom levels</value>
<data name="Label_SkipSetup" xml:space="preserve">
<value>Skip first-time setup</value>
</data>
<data name="Label_Steps" xml:space="preserve">
<value>Steps</value>
<data name="Label_UnexpectedErrorOccurred" xml:space="preserve">
<value>An unexpected error occurred</value>
</data>
<data name="Label_StepsBase" xml:space="preserve">
<value>Steps - Base</value>
<data name="Action_ExitApplication" xml:space="preserve">
<value>Exit Application</value>
</data>
<data name="Label_StepsRefiner" xml:space="preserve">
<value>Steps - Refiner</value>
<data name="Label_DisplayName" xml:space="preserve">
<value>Display Name</value>
</data>
<data name="Label_CFGScale" xml:space="preserve">
<value>CFG Scale</value>
<data name="Label_InstallationWithThisNameExists" xml:space="preserve">
<value>An installation with this name already exists.</value>
</data>
<data name="Label_DenoisingStrength" xml:space="preserve">
<value>Denoising Strength</value>
<data name="Label_PleaseChooseDifferentName" xml:space="preserve">
<value>Please choose a different name or select a different install location.</value>
</data>
<data name="Label_Width" xml:space="preserve">
<value>Width</value>
<data name="Label_AdvancedOptions" xml:space="preserve">
<value>Advanced Options</value>
</data>
<data name="Label_Height" xml:space="preserve">
<value>Height</value>
<data name="Label_Commit" xml:space="preserve">
<value>Commit</value>
</data>
<data name="Label_Refiner" xml:space="preserve">
<value>Refiner</value>
<data name="Label_SharedModelFolderStrategy" xml:space="preserve">
<value>Shared Model Folder Strategy</value>
</data>
<data name="Label_VAE" xml:space="preserve">
<value>VAE</value>
<data name="Label_PyTorchVersion" xml:space="preserve">
<value>PyTorch Version</value>
</data>
<data name="Label_Model" xml:space="preserve">
<value>Model</value>
</data>
<data name="Action_Connect" xml:space="preserve">
<value>Connect</value>
</data>
<data name="Label_ConnectingEllipsis" xml:space="preserve">
<value>Connecting...</value>
<data name="Label_CloseDialogWhenFinished" xml:space="preserve">
<value>Close dialog when finished</value>
</data>
<data name="Action_Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="Label_WaitingToConnectEllipsis" xml:space="preserve">
<value>Waiting to connect...</value>
<data name="Label_DataDirectory" xml:space="preserve">
<value>Data Directory</value>
</data>
<data name="Label_DataDirectoryExplanation" xml:space="preserve">
<value>This is where model checkpoints, LORAs, web UIs, settings, etc. will be installed.</value>
</data>
<data name="Label_FatWarning" xml:space="preserve">
<value>You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience.</value>
</data>
<data name="Label_PortableMode" xml:space="preserve">
<value>Portable Mode</value>
</data>
<data name="Label_PortableModeExplanation" xml:space="preserve">
<value>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.</value>
</data>
<data name="Action_Continue" xml:space="preserve">
<value>Continue</value>
</data>
<data name="Label_PreviousImage" xml:space="preserve">
<value>Previous Image</value>
</data>
<data name="Label_NextImage" xml:space="preserve">
<value>Next Image</value>
</data>
<data name="Label_ModelDescription" xml:space="preserve">
<value>Model Description</value>
</data>
<data name="Label_NewVersionAvailable" xml:space="preserve">
<value>A new version of Stability Matrix is available!</value>
</data>
<data name="Label_ImportLatest" xml:space="preserve">
<value>Import Latest -</value>
</data>
<data name="Label_AllVersions" xml:space="preserve">
<value>All Versions</value>
</data>
<data name="Label_ModelSearchWatermark" xml:space="preserve">
<value>Search models, #tags, or @users</value>
</data>
<data name="Action_Search" xml:space="preserve">
<value>Search</value>
</data>
<data name="Label_Sort" xml:space="preserve">
<value>Sort</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>Period</value>
</data>
<data name="Label_ModelType" xml:space="preserve">
<value>Model Type</value>
</data>
<data name="Label_BaseModel" xml:space="preserve">
<value>Base Model</value>
</data>
<data name="Label_ShowNsfwContent" xml:space="preserve">
<value>Show NSFW Content</value>
</data>
<data name="Label_DataProvidedByCivitAi" xml:space="preserve">
<value>Data provided by CivitAI</value>
</data>
<data name="Label_Page" xml:space="preserve">
<value>Page</value>
</data>
<data name="Label_FirstPage" xml:space="preserve">
<value>First Page</value>
</data>
<data name="Label_PreviousPage" xml:space="preserve">
<value>Previous Page</value>
</data>
<data name="Label_NextPage" xml:space="preserve">
<value>Next Page</value>
</data>
<data name="Label_LastPage" xml:space="preserve">
<value>Last Page</value>
</data>
<data name="Action_Rename" xml:space="preserve">
<value>Rename</value>
</data>
<data name="Action_Delete" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Action_OpenOnCivitAi" xml:space="preserve">
<value>Open on CivitAI</value>
</data>
<data name="Label_ConnectedModel" xml:space="preserve">
<value>Connected Model</value>
</data>
<data name="Label_LocalModel" xml:space="preserve">
<value>Local Model</value>
</data>
<data name="Action_ShowInExplorer" xml:space="preserve">
<value>Show in Explorer</value>
</data>
<data name="Action_New" xml:space="preserve">
<value>New</value>
</data>
<data name="Label_Folder" xml:space="preserve">
<value>Folder</value>
</data>
<data name="Label_DropFileToImport" xml:space="preserve">
<value>Drop file here to import</value>
</data>
<data name="Label_ImportAsConnected" xml:space="preserve">
<value>Import as Connected</value>
</data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Search for connected metadata on new local imports</value>
</data>
<data name="Label_Indexing" xml:space="preserve">
<value>Indexing...</value>
</data>
<data name="Label_ModelsFolder" xml:space="preserve">
<value>Models Folder</value>
</data>
<data name="Label_Categories" xml:space="preserve">
<value>Categories</value>
</data>
<data name="Label_LetsGetStarted" xml:space="preserve">
<value>Let's get started</value>
</data>
<data name="Label_ReadAndAgree" xml:space="preserve">
<value>I have read and agree to the</value>
</data>
<data name="Label_LicenseAgreement" xml:space="preserve">
<value>License Agreement.</value>
</data>
<data name="Label_FindConnectedMetadata" xml:space="preserve">
<value>Find Connected Metadata</value>
</data>
<data name="Label_ShowModelImages" xml:space="preserve">
<value>Show Model Images</value>
</data>
</root>

164
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<string> 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)
/// </summary>
public static IEnumerable<CheckpointFile> FromDirectoryIndex(string directory, SearchOption searchOption = SearchOption.TopDirectoryOnly)
public static IEnumerable<CheckpointFile> 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<CheckpointFile> 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
/// <summary>
/// Index with progress reporting.
/// </summary>
public static IEnumerable<CheckpointFile> FromDirectoryIndex(string directory, IProgress<ProgressReport> progress,
SearchOption searchOption = SearchOption.TopDirectoryOnly)
public static IEnumerable<CheckpointFile> FromDirectoryIndex(
string directory,
IProgress<ProgressReport> 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<CheckpointFile>
{
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<CheckpointFile> FilePathComparer { get; } =
new FilePathEqualityComparer();
}

349
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<CheckpointFolder, string> SubFoldersCache { get; } =
new(x => x.DirectoryPath);
private readonly SourceCache<CheckpointFile, string> checkpointFilesCache =
new(x => x.FilePath);
// ReSharper disable once FieldCanBeMadeReadOnly.Local
private bool useCategoryVisibility;
/// <summary>
/// Absolute path to the folder.
/// </summary>
@ -50,12 +61,14 @@ public partial class CheckpointFolder : ViewModelBase
/// <summary>
/// True if the category is enabled for the manager page.
/// </summary>
[ObservableProperty] private bool isCategoryEnabled = true;
[ObservableProperty]
private bool isCategoryEnabled = true;
/// <summary>
/// True if currently expanded in the UI.
/// </summary>
[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<CheckpointFolder> SubFolders { get; init; } = new();
public AdvancedObservableList<CheckpointFile> CheckpointFiles { get; init; } = new();
public AdvancedObservableList<CheckpointFile> DisplayedCheckpointFiles { get; set; }
public IObservableCollection<CheckpointFolder> SubFolders { get; } =
new ObservableCollectionExtended<CheckpointFolder>();
public IObservableCollection<CheckpointFile> CheckpointFiles { get; } =
new ObservableCollectionExtended<CheckpointFile>();
public IObservableCollection<CheckpointFile> DisplayedCheckpointFiles { get; } =
new ObservableCollectionExtended<CheckpointFile>();
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<CheckpointFile>
.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;
}
/// <summary>
/// When title is set, set the category enabled state from settings.
/// </summary>
// 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<CheckpointFile>(filteredFiles);
subFolder.SearchFilter = value;
}
checkpointFilesCache.Refresh();
}
/// <summary>
@ -130,13 +176,14 @@ public partial class CheckpointFolder : ViewModelBase
/// </summary>
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<TaskDialogButton>
@ -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,
}
);
}
}
/// <summary>
/// Imports files to the folder. Reports progress to instance properties.
/// </summary>
public async Task ImportFilesAsync(IEnumerable<string> files, bool convertToConnected = false)
public async Task ImportFilesAsync(
IEnumerable<string> 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<ProgressReport>(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<Task<bool>>();
@ -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
);
}
}
/// <summary>
/// Clears progress after a delay.
/// </summary>
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;
});
}
/// <summary>
/// Gets checkpoint files from folder index
/// </summary>
private async Task<List<CheckpointFile>> GetCheckpointFilesAsync(IProgress<ProgressReport>? progress = default)
private IEnumerable<CheckpointFile> GetCheckpointFiles()
{
if (!Directory.Exists(DirectoryPath))
{
return new List<CheckpointFile>();
return Enumerable.Empty<CheckpointFile>();
}
return await (progress switch
{
null => Task.Run(() =>
CheckpointFile.FromDirectoryIndex(DirectoryPath).ToList()),
_ => Task.Run(() =>
CheckpointFile.FromDirectoryIndex(DirectoryPath, progress).ToList())
});
return CheckpointFile.FromDirectoryIndex(DirectoryPath);
}
/// <summary>
/// Indexes the folder for checkpoint files and refreshes the CheckPointFiles collection.
/// </summary>
public void BackgroundIndex()
{
Dispatcher.UIThread.Post(Index, DispatcherPriority.Background);
}
/// <summary>
/// Indexes the folder for checkpoint files and refreshes the CheckPointFiles collection.
/// </summary>
public async Task IndexAsync(IProgress<ProgressReport>? 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
);
}
}

194
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<CheckpointManager.CheckpointFolder> checkpointFolders = new();
public SourceCache<CheckpointFolder, string> CheckpointFoldersCache { get; } =
new(x => x.DirectoryPath);
[ObservableProperty]
private ObservableCollection<CheckpointManager.CheckpointFolder> displayedCheckpointFolders = new();
public IObservableCollection<CheckpointFolder> CheckpointFolders { get; } =
new ObservableCollectionExtended<CheckpointFolder>();
public IObservableCollection<CheckpointFolder> DisplayedCheckpointFolders { get; } =
new ObservableCollectionExtended<CheckpointFolder>();
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<CheckpointFolder>(
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<CheckpointFolder>(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<CheckpointFolder>(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<CheckpointFolder>(filtered);
}
else
{
DisplayedCheckpointFolders = CheckpointFolders;
}
Logger.Info($"IndexFolders in {sw.Elapsed.TotalMilliseconds:F1}ms");
}
[RelayCommand]

31
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">
<Button.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Import Latest -" />
<TextBlock Text="{x:Static lang:Resources.Label_ImportLatest}" />
<TextBlock Margin="4,0,0,0"
Text="{Binding CivitModel.FullFilesSize}" />
</StackPanel>
@ -162,7 +163,7 @@
IsEnabled="{Binding !IsImporting}"
Command="{Binding ShowVersionDialogCommand}"
CommandParameter="{Binding CivitModel}"
Content="All Versions" />
Content="{x:Static lang:Resources.Label_AllVersions}" />
</Grid>
</StackPanel>
</controls:Card>
@ -177,7 +178,7 @@
<TextBox
HorizontalAlignment="Stretch"
Margin="8,0,0,0"
Watermark="Search models, #tags, or @users"
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}"
Text="{Binding SearchQuery, Mode=TwoWay}" />
<Button
@ -197,7 +198,7 @@
IsVisible="{Binding SearchModelsCommand.IsRunning}"
MinWidth="16" />
<TextBlock
Text="Search"
Text="{x:Static lang:Resources.Action_Search}"
VerticalAlignment="Center"
IsVisible="{Binding !SearchModelsCommand.IsRunning}" />
</Grid>
@ -205,7 +206,7 @@
</Grid>
<DockPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Sort" />
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
ItemsSource="{Binding AllSortModes}"
MinWidth="100"
@ -213,7 +214,7 @@
</StackPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Period" />
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" />
<ComboBox
ItemsSource="{Binding AllCivitPeriods}"
MinWidth="100"
@ -221,7 +222,7 @@
</StackPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Model Type" />
<Label Content="{x:Static lang:Resources.Label_ModelType}" />
<ComboBox
ItemsSource="{Binding AllModelTypes}"
MinWidth="100"
@ -229,7 +230,7 @@
</StackPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Base Model" />
<Label Content="{x:Static lang:Resources.Label_BaseModel}" />
<ComboBox
ItemsSource="{Binding BaseModelOptions}"
MinWidth="100"
@ -238,7 +239,7 @@
<CheckBox
Content="Show NSFW Content"
Content="{x:Static lang:Resources.Label_ShowNsfwContent}"
HorizontalAlignment="Right"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}"
Margin="8,8,8,0" />
@ -258,7 +259,7 @@
<TextBlock
Grid.Row="2"
Margin="16,8"
Text="Data provided by CivitAI"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}"
VerticalAlignment="Bottom" />
<StackPanel
@ -268,7 +269,7 @@
Orientation="Vertical"
IsVisible="{Binding HasSearched}">
<TextBlock Margin="0,0,4,4" TextAlignment="Center">
<Run Text="Page" />
<Run Text="{x:Static lang:Resources.Label_Page}" />
<Run Text="{Binding CurrentPageNumber, FallbackValue=1}" />
<Run Text="/" />
<Run Text="{Binding TotalPages, FallbackValue=5}" />
@ -277,26 +278,26 @@
<Button
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="First Page"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}"
Margin="0,0,8,0">
<avalonia:Icon Value="fa-solid fa-backward-fast"/>
</Button>
<Button
Command="{Binding PreviousPage}"
ToolTip.Tip="Previous Page"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
Margin="0,0,8,0">
<avalonia:Icon Value="fa-solid fa-caret-left"/>
</Button>
<Button Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="Next Page"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}"
Margin="0,0,8,0">
<avalonia:Icon Value="fa-solid fa-caret-right"/>
</Button>
<Button
Command="{Binding LastPage}"
ToolTip.Tip="Last Page"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}"
IsEnabled="{Binding CanGoToLastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast"/>
</Button>

89
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -54,23 +54,23 @@
<controls:Card.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem Command="{Binding RenameCommand}"
Text="Rename" IconSource="Rename" />
Text="{x:Static lang:Resources.Action_Rename}" IconSource="Rename" />
<ui:MenuFlyoutItem Command="{Binding DeleteCommand}"
Text="Delete" IconSource="Delete" />
Text="{x:Static lang:Resources.Action_Delete}" IconSource="Delete" />
<ui:MenuFlyoutItem Command="{Binding OpenOnCivitAiCommand}"
Text="Open on CivitAI" IconSource="Link"
Text="{x:Static lang:Resources.Action_OpenOnCivitAi}" IconSource="Link"
IsVisible="{Binding IsConnectedModel}"/>
</ui:FAMenuFlyout>
</controls:Card.ContextFlyout>
<Grid>
<!-- Main contents, hidden when IsLoading is true -->
<StackPanel MinHeight="70">
<Grid ColumnDefinitions="*,*,*,*,*" RowDefinitions="*,0.1*" IsVisible="{Binding !IsLoading}">
<Grid ColumnDefinitions="*,*,*,*,*" RowDefinitions="*,Auto,0.1*" IsVisible="{Binding !IsLoading}">
<StackPanel
Grid.ColumnSpan="4"
HorizontalAlignment="Left"
IsEnabled="True"
Margin="4"
Margin="0,0,0,4"
Orientation="Vertical">
<!-- Tooltip for title and version -->
<ToolTip.Tip>
@ -119,6 +119,16 @@
TextWrapping="WrapWithOverflow"
IsVisible="{Binding IsConnectedModel}" />
</StackPanel>
<controls:BetterAdvancedImage Grid.Row="1"
Grid.Column="0" Grid.ColumnSpan="5"
Margin="0,0,0,4"
CornerRadius="4"
Source="{Binding PreviewImagePath}"
Stretch="UniformToFill"
MaxHeight="250"
IsVisible="{Binding $parent[ItemsControl].((vm:CheckpointsPageViewModel)DataContext).ShowConnectedModelImages}" />
<Grid Grid.Column="4" Grid.Row="0">
<Button
Background="Transparent"
@ -135,7 +145,7 @@
<ui:SymbolIcon
Foreground="LightGreen"
Symbol="CloudSyncComplete"
ToolTip.Tip="Connected Model" />
ToolTip.Tip="{x:Static lang:Resources.Label_ConnectedModel}" />
</Button>
<Button
Background="Transparent"
@ -152,7 +162,7 @@
<ui:SymbolIcon
Foreground="DimGray"
Symbol="CloudOffline"
ToolTip.Tip="Local Model" />
ToolTip.Tip="{x:Static lang:Resources.Label_LocalModel}" />
</Button>
</Grid>
<TextBlock
@ -160,29 +170,25 @@
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
Grid.Row="2"
IsEnabled="True"
Margin="4,4"
Text="{Binding FileName}"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding FileName}"
VerticalAlignment="Bottom" />
<ItemsControl
<ItemsRepeater
Grid.Column="3"
Grid.ColumnSpan="2"
Grid.Row="1"
Grid.Row="2"
ItemTemplate="{StaticResource BadgeTemplate}"
ItemsSource="{Binding Badges}"
VerticalAlignment="Bottom">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
HorizontalAlignment="Right"
Orientation="Horizontal"
VerticalAlignment="Bottom" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<ItemsRepeater.Layout>
<UniformGridLayout/>
</ItemsRepeater.Layout>
</ItemsRepeater>
</Grid>
</StackPanel>
@ -215,21 +221,24 @@
<!-- Right click menu for a checkpoint folder -->
<Expander.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem Text="Show in Explorer" IconSource="Open"
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Action_ShowInExplorer}" IconSource="Open"
Command="{Binding ShowInExplorerCommand}"
CommandParameter="{Binding DirectoryPath}"/>
<!-- Only allow deletion of non-root folders (Parent is not null) -->
<ui:MenuFlyoutItem Text="Delete" IconSource="Delete"
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Action_Delete}" IconSource="Delete"
IsEnabled="{Binding ParentFolder, Converter={x:Static ObjectConverters.IsNotNull}}"
IsVisible="{Binding ParentFolder, Converter={x:Static ObjectConverters.IsNotNull}}"
Command="{Binding DeleteCommand}"/>
<ui:MenuFlyoutSeparator/>
<ui:MenuFlyoutSubItem Text="New" IconSource="Add">
<ui:MenuFlyoutSubItem Text="{x:Static lang:Resources.Action_New}" IconSource="Add">
<ui:MenuFlyoutSubItem.Items>
<ui:MenuFlyoutItem Text="Folder" IconSource="OpenFolder"
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Label_Folder}" IconSource="OpenFolder"
Command="{Binding CreateSubFolderCommand}"/>
</ui:MenuFlyoutSubItem.Items>
</ui:MenuFlyoutSubItem>
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Label_FindConnectedMetadata}"
IconSource="Find"
Command="{Binding FindConnectedMetadata}"/>
</ui:FAMenuFlyout>
</Expander.ContextFlyout>
@ -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}" />
<StackPanel
@ -355,15 +364,23 @@
</controls:UserControlBase.Resources>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="0.2*,*" Margin="4, 0">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*" Margin="4, 0">
<!-- Top settings bar -->
<ToggleSwitch
OffContent="Import as Connected"
OnContent="Import as Connected"
Grid.Column="0"
IsChecked="{Binding IsImportAsConnected}"
Margin="24,8"
ToolTip.Tip="Search for connected metadata on new local imports" />
<StackPanel Grid.Column="0" Grid.Row="0">
<ToggleSwitch
OffContent="{x:Static lang:Resources.Label_ImportAsConnected}"
OnContent="{x:Static lang:Resources.Label_ImportAsConnected}"
IsChecked="{Binding IsImportAsConnected}"
Margin="24,4,24,-4"
ToolTip.Tip="{x:Static lang:Resources.Label_ImportAsConnectedExplanation}" />
<ToggleSwitch
OffContent="{x:Static lang:Resources.Label_ShowModelImages}"
OnContent="{x:Static lang:Resources.Label_ShowModelImages}"
IsChecked="{Binding ShowConnectedModelImages}"
Margin="24,-4,24,4" />
</StackPanel>
<StackPanel
Grid.Column="1"
Grid.Row="0"
@ -372,7 +389,7 @@
Margin="16,0"
Orientation="Horizontal">
<TextBox Margin="16, 16"
Watermark="Search"
Watermark="{x:Static lang:Resources.Action_Search}"
Height="16"
MinWidth="150"
Text="{Binding SearchFilter, Mode=TwoWay}"/>
@ -382,15 +399,15 @@
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding IsIndexing}"/>
<TextBlock Text="Indexing..." FontSize="11"
<TextBlock Text="{x:Static lang:Resources.Label_Indexing}" FontSize="11"
VerticalAlignment="Center"
IsVisible="{Binding IsIndexing}"/>
<Button
Command="{Binding OpenModelsFolderCommand}">
<TextBlock Text="Models Folder"/>
<TextBlock Text="{x:Static lang:Resources.Label_ModelsFolder}"/>
</Button>
<DropDownButton
Content="Categories"
Content="{x:Static lang:Resources.Label_Categories}"
Margin="8"
VerticalAlignment="Center"
HorizontalAlignment="Right">

5
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 @@
<Button
Name="ExitButton"
IsDefault="True"
Content="Exit Application"
Content="{x:Static lang:Resources.Action_ExitApplication}"
Click="ExitButton_OnClick"
Grid.Row="2"
HorizontalAlignment="Right"

23
StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml

@ -9,6 +9,7 @@
xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
x:DataType="dialogs:InstallerViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700"
d:DataContext="{x:Static mocks:DesignData.InstallerViewModel}"
@ -49,7 +50,7 @@
<Label Grid.Row="1" Grid.Column="0"
Margin="8,0"
Content="Display Name" />
Content="{x:Static lang:Resources.Label_DisplayName}" />
<TextBox Grid.Row="2" Grid.Column="0"
MinWidth="250"
Margin="8,0"
@ -69,9 +70,9 @@
Foreground="{DynamicResource ThemeRedColor}"
TextAlignment="Left"
TextWrapping="Wrap">
<Run Text="An installation with this name already exists." />
<Run Text="{x:Static lang:Resources.Label_InstallationWithThisNameExists}" />
<LineBreak />
<Run Text="Please choose a different name or select a different Install Location." />
<Run Text="{x:Static lang:Resources.Label_PleaseChooseDifferentName}" />
</TextBlock>
</StackPanel>
@ -98,11 +99,11 @@
Orientation="Horizontal" Margin="0,8,0,8"
IsVisible="{Binding IsReleaseModeAvailable}">
<ToggleButton
Content="Releases"
Content="{x:Static lang:Resources.Label_Releases}"
IsChecked="{Binding IsReleaseMode}"
IsEnabled="{Binding IsReleaseModeAvailable}" />
<ToggleButton
Content="Branches"
Content="{x:Static lang:Resources.Label_Branches}"
IsChecked="{Binding !IsReleaseMode}"
Margin="8,0,0,0" />
</StackPanel>
@ -118,12 +119,12 @@
Margin="8"
IsVisible="{Binding !IsAdvancedMode}">
<Label Content="Advanced Options"
<Label Content="{x:Static lang:Resources.Label_AdvancedOptions}"
FontSize="18"
HorizontalAlignment="Center"
Margin="8,0,8,8" />
<Label Content="Commit"
<Label Content="{x:Static lang:Resources.Label_Commit}"
Margin="0,0,0,4"
IsVisible="{Binding !IsReleaseMode}" />
<ui:FAComboBox
@ -142,7 +143,7 @@
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
<Label Content="Shared Model Folder Strategy"
<Label Content="{x:Static lang:Resources.Label_SharedModelFolderStrategy}"
Margin="0,8,0,4" />
<ui:FAComboBox
ItemsSource="{Binding SelectedPackage.AvailableSharedFolderMethods}"
@ -158,7 +159,7 @@
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
<Label Content="PyTorch Version"
<Label Content="{x:Static lang:Resources.Label_PyTorchVersion}"
IsVisible="{Binding ShowTorchVersionOptions}"
Margin="0,8,0,4" />
<ui:FAComboBox
@ -187,14 +188,14 @@
Margin="0,32,0,0"
HorizontalAlignment="Center">
<Button
Content="Cancel"
Content="{x:Static lang:Resources.Action_Cancel}"
Command="{Binding Cancel}"
FontSize="20"
HorizontalAlignment="Center"
Margin="8,0,4,0"
Padding="16, 8, 16, 8" />
<Button
Content="Install"
Content="{x:Static lang:Resources.Action_Install}"
Command="{Binding InstallCommand}"
FontSize="20"
IsEnabled="{Binding CanInstall}"

5
StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml

@ -6,6 +6,7 @@
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="700"
x:DataType="dialogs:OneClickInstallViewModel"
d:DataContext="{x:Static designData:DesignData.OneClickInstallViewModel}"
@ -87,7 +88,7 @@
IsVisible="{Binding ShowInstallButton}"
HorizontalAlignment="Center">
<Button
Content="Install"
Content="{x:Static lang:Resources.Action_Install}"
Command="{Binding InstallCommand}"
FontSize="32"
HorizontalAlignment="Center"
@ -95,7 +96,7 @@
Margin="16"
Padding="16, 8, 16, 8" />
<Button Command="{Binding ToggleAdvancedModeCommand}"
Content="Skip first-time setup"
Content="{x:Static lang:Resources.Label_SkipSetup}"
FontSize="16"
HorizontalAlignment="Center"
Padding="16, 8, 16, 8"/>

5
StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml

@ -6,6 +6,7 @@
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:base="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="base:ContentDialogProgressViewModelBase"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PackageModificationDialog">
@ -46,9 +47,9 @@
<CheckBox Grid.Row="4" IsChecked="{Binding CloseWhenFinished}"
HorizontalAlignment="Center"
Margin="4" Content="Close dialog when finished"/>
Margin="4" Content="{x:Static lang:Resources.Label_CloseDialogWhenFinished}"/>
<Button Grid.Row="5" Content="Close"
<Button Grid.Row="5" Content="{x:Static lang:Resources.Action_Close}"
FontSize="20"
HorizontalAlignment="Center"
Command="{Binding OnCloseButtonClick}"/>

13
StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml

@ -6,6 +6,7 @@
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.SelectDataDirectoryViewModel}"
x:DataType="dialogs:SelectDataDirectoryViewModel"
x:CompileBindings="True"
@ -26,7 +27,7 @@
Title="Welcome back! In this update, you can optionally choose a custom location to store all data. If you choose a new location, or opt to use Portable Mode, you'll be able to migrate your existing data on the next page."/>
<Label
Content="Data Directory"
Content="{x:Static lang:Resources.Label_DataDirectory}"
FontSize="13"
Margin="0,16,0,0" />
<Grid ColumnDefinitions="*,Auto">
@ -50,14 +51,14 @@
</Grid>
<TextBlock
Text="This is where the model checkpoints, LORAs, web UIs, settings, etc. will be installed."
Text="{x:Static lang:Resources.Label_DataDirectoryExplanation}"
TextWrapping="Wrap"
Foreground="LightGray"
FontSize="12"
Margin="0,8,0,0" />
<TextBlock
Text="You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience."
Text="{x:Static lang:Resources.Label_FatWarning}"
TextWrapping="Wrap"
Foreground="Yellow"
IsVisible="{Binding ShowFatWarning}"
@ -65,7 +66,7 @@
Margin="0,8,0,0" />
<CheckBox
Content="Portable Mode"
Content="{x:Static lang:Resources.Label_PortableMode}"
IsChecked="{Binding IsPortableMode, Mode=TwoWay}"
Margin="0,32,0,0" />
@ -75,7 +76,7 @@
FontSize="13"
Margin="0,8,0,0"
Padding="16"
Title="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." />
Title="{x:Static lang:Resources.Label_PortableModeExplanation}" />
</StackPanel>
@ -94,7 +95,7 @@
</StackPanel>
<Button
Content="Continue"
Content="{x:Static lang:Resources.Action_Continue}"
IsDefault="True"
Command="{Binding OnPrimaryButtonClick}"
FontSize="16"

11
StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml

@ -10,6 +10,7 @@
xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static designData:DesignData.SelectModelVersionViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.SelectModelVersionDialog">
@ -64,13 +65,13 @@
<Button Command="{Binding PreviousImage}"
Margin="0,0,8,0"
IsEnabled="{Binding CanGoToPreviousImage}"
ToolTip.Tip="Previous Image"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousImage}"
ToolTip.Placement="Top">
<avalonia:Icon Value="fa-solid fa-caret-left"/>
</Button>
<Button Command="{Binding NextImage}"
IsEnabled="{Binding CanGoToNextImage}"
ToolTip.Tip="Next Image"
ToolTip.Tip="{x:Static lang:Resources.Label_NextImage}"
ToolTip.Placement="Top">
<avalonia:Icon Value="fa-solid fa-caret-right"/>
</Button>
@ -164,7 +165,7 @@
<Expander Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
Header="Model Description"
Header="{x:Static lang:Resources.Label_ModelDescription}"
ExpandDirection="Down"
Margin="8, 16">
<ScrollViewer MaxHeight="200">
@ -179,14 +180,14 @@
Margin="0,8,0,0"
HorizontalAlignment="Center">
<Button
Content="Cancel"
Content="{x:Static lang:Resources.Action_Cancel}"
HorizontalAlignment="Center"
Command="{Binding Cancel}"
Margin="8, 0" />
<Button
Margin="8, 0"
Content="Import"
Content="{x:Static lang:Resources.Action_Import}"
Command="{Binding Import}"
IsEnabled="{Binding IsImportEnabled}"
Classes="accent" />

3
StabilityMatrix.Avalonia/Views/Dialogs/UpdateDialog.axaml

@ -7,6 +7,7 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.UpdateViewModel}"
x:DataType="dialogs:UpdateViewModel"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="550"
@ -20,7 +21,7 @@
</TitleBar>-->
<TextBlock Grid.Row="0"
Text="A new version of Stability Matrix is available!"
Text="{x:Static lang:Resources.Label_NewVersionAvailable}"
HorizontalAlignment="Center"
FontWeight="Thin"
Margin="0,16,0,0"

11
StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml

@ -6,6 +6,7 @@
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
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.FirstLaunchSetupViewModel}"
x:DataType="vm:FirstLaunchSetupViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
@ -28,7 +29,7 @@
<Grid Margin="0,16,32,32" RowDefinitions="0.6*,0.4*">
<!-- Texts -->
<StackPanel Margin="32,64,0,0">
<TextBlock FontSize="24" Text="Let's get started" />
<TextBlock FontSize="24" Text="{x:Static lang:Resources.Label_LetsGetStarted}" />
<!-- Loading icon and text -->
<Grid Margin="0,8,0,0" ColumnDefinitions="0.15*,*">
<controls:RefreshBadge
@ -79,7 +80,7 @@
<TextBlock
VerticalAlignment="Top"
FontSize="14"
Text="I have read and agree to the"/>
Text="{x:Static lang:Resources.Label_ReadAndAgree}"/>
<ui:HyperlinkButton
VerticalAlignment="Top"
@ -87,7 +88,7 @@
Margin="1,0,0,0"
Classes="transparent"
NavigateUri="https://lykos.ai/matrix/license"
Content="License Agreement."
Content="{x:Static lang:Resources.Label_LicenseAgreement}"
Grid.Column="1" />
</Grid>
</CheckBox>
@ -102,11 +103,11 @@
<Button
Classes="danger"
Click="QuitButton_OnClick"
Content="Quit" />
Content="{x:Static lang:Resources.Action_Quit}" />
<Button
Classes="info"
Click="ContinueButton_OnClick"
Content="Continue"
Content="{x:Static lang:Resources.Action_Continue}"
IsEnabled="{Binding EulaAccepted, Mode=OneWay}" />
</UniformGrid>

9
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -8,6 +8,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:base="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550"
d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}"
x:DataType="vm:MainWindowViewModel"
@ -81,7 +82,7 @@
<StackPanel>
<!-- Update button -->
<ui:NavigationViewItem Name="FooterUpdateItem"
Content="Update Available"
Content="{x:Static lang:Resources.Label_UpdateAvailable}"
IsVisible="{Binding IsUpdateAvailable}"
IsEnabled="{Binding IsUpdateAvailable}"
DataContext="{Binding UpdateViewModel}"
@ -93,7 +94,7 @@
<!-- Patreon -->
<ui:NavigationViewItem Name="PatreonPatreonItem"
Content="Become a Patron"
Content="{x:Static lang:Resources.Label_BecomeAPatron}"
Tapped="PatreonPatreonItem_OnTapped">
<ui:NavigationViewItem.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-patreon"/>
@ -102,7 +103,7 @@
<!-- Discord -->
<ui:NavigationViewItem Name="FooterDiscordItem"
Content="Discord Server"
Content="{x:Static lang:Resources.Label_JoinDiscord}"
Tapped="FooterDiscordItem_OnTapped">
<ui:NavigationViewItem.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-discord"/>
@ -111,7 +112,7 @@
<!-- Download flyout -->
<ui:NavigationViewItem Name="FooterDownloadItem"
Content="Downloads"
Content="{x:Static lang:Resources.Label_Downloads}"
IconSource="{Binding IconSource}"
DataContext="{Binding ProgressManagerViewModel}"
IsVisible="{Binding !!ProgressItems.Count}"

2
StabilityMatrix.Core/Models/Settings/Settings.cs

@ -38,6 +38,7 @@ public class Settings
public bool ModelBrowserNsfwEnabled { get; set; }
public bool IsNavExpanded { get; set; }
public bool IsImportAsConnected { get; set; }
public bool ShowConnectedModelImages { get; set; }
public SharedFolderType? SharedFolderVisibleCategories { get; set; } =
SharedFolderType.StableDiffusion | SharedFolderType.Lora | SharedFolderType.LyCORIS;
@ -64,7 +65,6 @@ public class Settings
/// Whether the Inference Image Viewer shows pixel grids at high zoom levels
/// </summary>
public bool IsImageViewerPixelGridEnabled { get; set; } = true;
public bool RemoveFolderLinksOnShutdown { get; set; }
public bool IsDiscordRichPresenceEnabled { get; set; }

180
StabilityMatrix/Models/CheckpointFolder.cs

@ -26,9 +26,10 @@ public partial class CheckpointFolder : ObservableObject
private readonly ISettingsManager settingsManager;
private readonly IDownloadService downloadService;
private readonly ModelFinder modelFinder;
// ReSharper disable once FieldCanBeMadeReadOnly.Local
private bool useCategoryVisibility;
/// <summary>
/// Absolute path to the folder.
/// </summary>
@ -58,55 +59,59 @@ public partial class CheckpointFolder : ObservableObject
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))]
private bool isImportInProgress;
public bool IsDragBlurEnabled => IsCurrentDragTarget || IsImportInProgress;
public string TitleWithFilesCount => CheckpointFiles.Any() ? $"{Title} ({CheckpointFiles.Count})" : Title;
public string TitleWithFilesCount =>
CheckpointFiles.Any() ? $"{Title} ({CheckpointFiles.Count})" : Title;
public ProgressViewModel Progress { get; } = new();
public ObservableCollection<CheckpointFolder> SubFolders { get; init; } = new();
public ObservableCollection<CheckpointFile> CheckpointFiles { get; init; } = new();
public RelayCommand OnPreviewDragEnterCommand => new(() => IsCurrentDragTarget = true);
public RelayCommand OnPreviewDragLeaveCommand => new(() => IsCurrentDragTarget = false);
public CheckpointFolder(
IDialogFactory dialogFactory,
IDialogFactory dialogFactory,
ISettingsManager settingsManager,
IDownloadService downloadService,
ModelFinder modelFinder,
bool useCategoryVisibility = true)
bool useCategoryVisibility = true
)
{
this.dialogFactory = dialogFactory;
this.settingsManager = settingsManager;
this.downloadService = downloadService;
this.modelFinder = modelFinder;
this.useCategoryVisibility = useCategoryVisibility;
CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged;
}
/// <summary>
/// When title is set, set the category enabled state from settings.
/// </summary>
// 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);
}
/// <summary>
/// When toggling the category enabled state, save it to settings.
/// </summary>
partial void OnIsCategoryEnabledChanged(bool value)
{
if (!useCategoryVisibility) return;
if (!useCategoryVisibility)
return;
if (value != settingsManager.IsSharedFolderCategoryVisible(FolderType))
{
settingsManager.SetSharedFolderCategoryVisible(FolderType, value);
@ -117,7 +122,8 @@ public partial class CheckpointFolder : ObservableObject
private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(TitleWithFilesCount));
if (e.NewItems == null) return;
if (e.NewItems == null)
return;
// On new added items, add event handler for deletion
foreach (CheckpointFile item in e.NewItems)
{
@ -155,33 +161,45 @@ public partial class CheckpointFolder : ObservableObject
{
Process.Start("explorer.exe", path);
}
/// <summary>
/// Imports files to the folder. Reports progress to instance properties.
/// </summary>
public async Task ImportFilesAsync(IEnumerable<string> files, bool convertToConnected = false)
public async Task ImportFilesAsync(
IEnumerable<string> files,
bool convertToConnected = false,
bool copyFiles = true
)
{
Progress.IsIndeterminate = true;
Progress.IsProgressVisible = true;
var copyPaths = files.ToDictionary(k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v)));
var copyPaths = files.ToDictionary(
k => k,
v => Path.Combine(DirectoryPath, Path.GetFileName(v))
);
var progress = new Progress<ProgressReport>(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<Task<bool>>();
@ -191,27 +209,42 @@ public partial class CheckpointFolder : ObservableObject
{
Progress.IsIndeterminate = false;
Progress.Value = report.Percentage;
Progress.Text = modelFilesCount > 1 ?
$"Computing metadata for {modelFile.Info.Name} ({i}/{modelFilesCount})" :
$"Computing metadata for {report.Title}";
Progress.Text =
modelFilesCount > 1
? $"Computing metadata for {modelFile.Info.Name} ({i}/{modelFilesCount})"
: $"Computing metadata for {report.Title}";
});
var hashBlake3 = await FileHash.GetBlake3Async(modelFile, hashProgress);
if (!string.IsNullOrWhiteSpace(hashBlake3))
{
settingsManager.Transaction(s =>
{
s.InstalledModelHashes ??= new HashSet<string>();
s.InstalledModelHashes.Add(hashBlake3);
});
}
// Start a task to query the model in background
var queryTask = Task.Run(async () =>
{
var result = await modelFinder.LocalFindModel(hashBlake3);
result ??= await modelFinder.RemoteFindModel(hashBlake3);
if (result is null) return false; // Not found
if (result is null)
return false; // Not found
var (model, version, file) = result.Value;
// Save connected model info json
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name);
var modelInfo = new ConnectedModelInfo(
model, version, file, DateTimeOffset.UtcNow);
model,
version,
file,
DateTimeOffset.UtcNow
);
await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName);
// If available, save thumbnail
@ -222,7 +255,8 @@ public partial class CheckpointFolder : ObservableObject
if (imageExt is "jpg" or "jpeg" or "png")
{
var imageDownloadPath = Path.GetFullPath(
Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}"));
Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}")
);
await downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
}
}
@ -231,30 +265,29 @@ public partial class CheckpointFolder : ObservableObject
});
modelQueryTasks.Add(queryTask);
}
// Set progress to indeterminate
Progress.IsIndeterminate = true;
Progress.Text = "Checking connected model information";
// Wait for all model queries to finish
var modelQueryResults = await Task.WhenAll(modelQueryTasks);
var successCount = modelQueryResults.Count(r => r);
var totalCount = modelQueryResults.Length;
var failCount = totalCount - successCount;
await IndexAsync();
Progress.Value = 100;
Progress.Text = successCount switch
{
0 when failCount > 0 =>
"Import complete. No connected data found.",
> 0 when failCount > 0 =>
$"Import complete. Found connected data for {successCount} of {totalCount} models.",
0 when failCount > 0 => "Import complete. No connected data found.",
> 0 when failCount > 0
=> $"Import complete. Found connected data for {successCount} of {totalCount} models.",
_ => $"Import complete. Found connected data for all {totalCount} models."
};
DelayedClearProgress(TimeSpan.FromSeconds(1));
}
else
@ -271,33 +304,46 @@ public partial class CheckpointFolder : ObservableObject
/// </summary>
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay).ContinueWith(_ =>
{
IsImportInProgress = false;
Progress.IsProgressVisible = false;
Progress.Value = 0;
Progress.Text = string.Empty;
});
Task.Delay(delay)
.ContinueWith(_ =>
{
IsImportInProgress = false;
Progress.IsProgressVisible = false;
Progress.Value = 0;
Progress.Text = string.Empty;
});
}
/// <summary>
/// Gets checkpoint files from folder index
/// </summary>
private async Task<List<CheckpointFile>> GetCheckpointFilesAsync(IProgress<ProgressReport>? progress = default)
private async Task<List<CheckpointFile>> GetCheckpointFilesAsync(
IProgress<ProgressReport>? progress = default
)
{
if (!Directory.Exists(DirectoryPath))
{
return new List<CheckpointFile>();
}
return await (progress switch
{
null => Task.Run(() =>
CheckpointFile.FromDirectoryIndex(dialogFactory, DirectoryPath).ToList()),
_ => Task.Run(() =>
CheckpointFile.FromDirectoryIndex(dialogFactory, DirectoryPath, progress).ToList())
});
return await (
progress switch
{
null
=> Task.Run(
() =>
CheckpointFile.FromDirectoryIndex(dialogFactory, DirectoryPath).ToList()
),
_
=> Task.Run(
() =>
CheckpointFile
.FromDirectoryIndex(dialogFactory, DirectoryPath, progress)
.ToList()
)
}
);
}
/// <summary>
@ -309,19 +355,23 @@ public partial class CheckpointFolder : ObservableObject
foreach (var folder in Directory.GetDirectories(DirectoryPath))
{
// Inherit our folder type
var subFolder = new CheckpointFolder(dialogFactory, settingsManager,
downloadService, modelFinder,
useCategoryVisibility: false)
var subFolder = new CheckpointFolder(
dialogFactory,
settingsManager,
downloadService,
modelFinder,
useCategoryVisibility: false
)
{
Title = Path.GetFileName(folder),
DirectoryPath = folder,
FolderType = FolderType
};
await subFolder.IndexAsync(progress);
SubFolders.Add(subFolder);
}
var checkpointFiles = await GetCheckpointFilesAsync();
CheckpointFiles.Clear();
foreach (var checkpointFile in checkpointFiles)

Loading…
Cancel
Save