Browse Source

Add settings subpage framework and breadcrumb bar

pull/324/head
Ionite 1 year ago
parent
commit
8b2c12ea22
No known key found for this signature in database
  1. 3
      StabilityMatrix.Avalonia/App.axaml
  2. 20
      StabilityMatrix.Avalonia/App.axaml.cs
  3. 4
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  4. 10
      StabilityMatrix.Avalonia/Models/TypedNavigationEventArgs.cs
  5. 8
      StabilityMatrix.Avalonia/Services/INavigationService.cs
  6. 37
      StabilityMatrix.Avalonia/Services/NavigationService.cs
  7. 1
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  8. 2
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  9. 4
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
  10. 4
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  11. 4
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  12. 4
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  13. 17
      StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
  14. 950
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  15. 901
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  16. 18
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  17. 67
      StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml
  18. 10
      StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml.cs
  19. 533
      StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
  20. 13
      StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml.cs
  21. 525
      StabilityMatrix.Avalonia/Views/SettingsPage.axaml
  22. 84
      StabilityMatrix.Avalonia/Views/SettingsPage.axaml.cs
  23. 11
      StabilityMatrix.Core/Attributes/SingletonAttribute.cs

3
StabilityMatrix.Avalonia/App.axaml

@ -27,6 +27,8 @@
<idcr:ControlRecycling x:Key="ControlRecyclingKey" />
<x:Double x:Key="ContentDialogMaxWidth">700</x:Double>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">32</x:Double>
<SolidColorBrush x:Key="ToolTipBackground" Color="#1E1F22"/>
<SolidColorBrush x:Key="ToolTipForeground" Color="#9FBDC3"/>
<FontFamily x:Key="NotoSansJP">avares://StabilityMatrix.Avalonia/Assets/Fonts/NotoSansJP#Noto Sans JP</FontFamily>
@ -39,6 +41,7 @@
<StyleInclude Source="avares://Dock.Avalonia/Themes/DockFluentTheme.axaml" />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml"/>
<StyleInclude Source="avares://AsyncImageLoader.Avalonia/AdvancedImage.axaml" />
<StyleInclude Source="avares://FluentAvalonia.BreadcrumbBar/Styling/Styles.axaml" />
<StyleInclude Source="Styles/ProgressRing.axaml"/>
<StyleInclude Source="Styles/ButtonStyles.axaml"/>
<StyleInclude Source="Styles/SplitButtonStyles.axaml"/>

20
StabilityMatrix.Avalonia/App.axaml.cs

@ -352,17 +352,31 @@ public sealed class App : Application
t1.attributes is { Length: > 0 }
&& !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attribute = (SingletonAttribute)t1.attributes[0] });
.Select(
t1 =>
new
{
Type = t1.t,
Attributes = t1.attributes.Cast<SingletonAttribute>().ToArray()
}
);
foreach (var typePair in singletonTypes)
{
if (typePair.Attribute.InterfaceType is null)
foreach (var attribute in typePair.Attributes)
{
if (attribute.InterfaceType is null)
{
services.AddSingleton(typePair.Type);
}
else if (attribute.ImplType is not null)
{
services.AddSingleton(attribute.InterfaceType, attribute.ImplType);
}
else
{
services.AddSingleton(typePair.Attribute.InterfaceType, typePair.Type);
services.AddSingleton(attribute.InterfaceType, typePair.Type);
}
}
}

4
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -103,7 +103,6 @@ public static class DesignData
// General services
services
.AddLogging()
.AddSingleton<INavigationService, NavigationService>()
.AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<IUpdateHelper, UpdateHelper>()
.AddSingleton<ModelFinder>()
@ -437,6 +436,9 @@ public static class DesignData
public static InferenceSettingsViewModel InferenceSettingsViewModel =>
Services.GetRequiredService<InferenceSettingsViewModel>();
public static MainSettingsViewModel MainSettingsViewModel =>
Services.GetRequiredService<MainSettingsViewModel>();
public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService<CheckpointBrowserViewModel>();

10
StabilityMatrix.Avalonia/Models/TypedNavigationEventArgs.cs

@ -0,0 +1,10 @@
using System;
namespace StabilityMatrix.Avalonia.Models;
public class TypedNavigationEventArgs : EventArgs
{
public required Type ViewModelType { get; init; }
public object? ViewModel { get; init; }
}

8
StabilityMatrix.Avalonia/Services/INavigationService.cs

@ -1,11 +1,15 @@
using FluentAvalonia.UI.Controls;
using System;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.Services;
public interface INavigationService
public interface INavigationService<T>
{
event EventHandler<TypedNavigationEventArgs>? TypedNavigation;
/// <summary>
/// Set the frame to use for navigation.
/// </summary>

37
StabilityMatrix.Avalonia/Services/NavigationService.cs

@ -4,6 +4,7 @@ using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Navigation;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
@ -11,11 +12,20 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Services;
[Singleton(typeof(INavigationService))]
public class NavigationService : INavigationService
[Singleton(
ImplType = typeof(NavigationService<MainWindowViewModel>),
InterfaceType = typeof(INavigationService<MainWindowViewModel>)
)]
[Singleton(
ImplType = typeof(NavigationService<SettingsViewModel>),
InterfaceType = typeof(INavigationService<SettingsViewModel>)
)]
public class NavigationService<T> : INavigationService<T>
{
private Frame? _frame;
public event EventHandler<TypedNavigationEventArgs>? TypedNavigation;
/// <inheritdoc />
public void SetFrame(Frame frame)
{
@ -60,19 +70,11 @@ public class NavigationService : INavigationService
}
);
if (!typeof(TViewModel).IsAssignableTo(typeof(PageViewModelBase)))
return;
if (
App.Services.GetService(typeof(MainWindowViewModel))
is MainWindowViewModel mainViewModel
)
{
mainViewModel.SelectedCategory = mainViewModel.Pages.FirstOrDefault(
x => x.GetType() == typeof(TViewModel)
TypedNavigation?.Invoke(
this,
new TypedNavigationEventArgs { ViewModelType = typeof(TViewModel) }
);
}
}
/// <inheritdoc />
public void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null)
@ -106,5 +108,14 @@ public class NavigationService : INavigationService
TransitionInfoOverride = transitionInfo ?? new SuppressNavigationTransitionInfo()
}
);
TypedNavigation?.Invoke(
this,
new TypedNavigationEventArgs
{
ViewModelType = viewModel.GetType(),
ViewModel = viewModel
}
);
}
}

1
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -38,6 +38,7 @@
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.2" />
<PackageReference Include="Dock.Serializer" Version="11.0.0.2" />
<PackageReference Include="DynamicData" Version="8.1.1" />
<PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.4" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.220" />
<PackageReference Include="FluentIcons.FluentAvalonia" Version="1.1.220" />

2
StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs

@ -127,7 +127,7 @@ public abstract partial class InferenceTabViewModelBase
// ResetViewState();
// TODO: Dock reset not working, using this hack for now to get a new view
var navService = App.Services.GetRequiredService<INavigationService>();
var navService = App.Services.GetRequiredService<INavigationService<MainWindowViewModel>>();
navService.NavigateTo<LaunchPageViewModel>(new SuppressNavigationTransitionInfo());
((IPersistentViewProvider)this).AttachedPersistentView = null;
navService.NavigateTo<InferenceViewModel>(new BetterEntranceNavigationTransition());

4
StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs

@ -27,7 +27,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly INavigationService navigationService;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly IPackageFactory packageFactory;
[ObservableProperty]
@ -56,7 +56,7 @@ public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBa
public InferenceConnectionHelpViewModel(
ISettingsManager settingsManager,
INavigationService navigationService,
INavigationService<MainWindowViewModel> navigationService,
IPackageFactory packageFactory
)
{

4
StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs

@ -30,7 +30,7 @@ public partial class OneClickInstallViewModel : ContentDialogViewModelBase
private readonly IPrerequisiteHelper prerequisiteHelper;
private readonly ILogger<OneClickInstallViewModel> logger;
private readonly IPyRunner pyRunner;
private readonly INavigationService navigationService;
private readonly INavigationService<MainWindowViewModel> navigationService;
private const string DefaultPackageName = "stable-diffusion-webui";
[ObservableProperty]
@ -71,7 +71,7 @@ public partial class OneClickInstallViewModel : ContentDialogViewModelBase
IPrerequisiteHelper prerequisiteHelper,
ILogger<OneClickInstallViewModel> logger,
IPyRunner pyRunner,
INavigationService navigationService
INavigationService<MainWindowViewModel> navigationService
)
{
this.settingsManager = settingsManager;

4
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -49,7 +49,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly ILogger<OutputsPageViewModel> logger;
public override string Title => Resources.Label_OutputsPageTitle;
@ -99,7 +99,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
ISettingsManager settingsManager,
IPackageFactory packageFactory,
INotificationService notificationService,
INavigationService navigationService,
INavigationService<MainWindowViewModel> navigationService,
ILogger<OutputsPageViewModel> logger
)
{

4
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs

@ -36,7 +36,7 @@ public partial class PackageCardViewModel : ProgressViewModel
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly INavigationService navigationService;
private readonly INavigationService<MainWindowViewModel> navigationService;
private readonly ServiceManager<ViewModelBase> vmFactory;
[ObservableProperty]
@ -80,7 +80,7 @@ public partial class PackageCardViewModel : ProgressViewModel
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
INavigationService navigationService,
INavigationService<MainWindowViewModel> navigationService,
ServiceManager<ViewModelBase> vmFactory
)
{

17
StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs

@ -1,9 +1,20 @@
using StabilityMatrix.Avalonia.ViewModels.Base;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Settings;
using StabilityMatrix.Core.Attributes;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels.Settings;
[View(typeof(InferenceSettingsPage))]
[Singleton]
public partial class InferenceSettingsViewModel : ViewModelBase { }
[Singleton, ManagedService]
public class InferenceSettingsViewModel : PageViewModelBase
{
/// <inheritdoc />
public override string Title => "Inference";
/// <inheritdoc />
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
}

950
StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs

@ -0,0 +1,950 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using NLog;
using SkiaSharp;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Avalonia.Views.Settings;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels.Settings;
[View(typeof(MainSettingsPage))]
[Singleton, ManagedService]
public partial class MainSettingsViewModel : PageViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly IPrerequisiteHelper prerequisiteHelper;
private readonly IPyRunner pyRunner;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ICompletionProvider completionProvider;
private readonly ITrackedDownloadService trackedDownloadService;
private readonly IModelIndexService modelIndexService;
private readonly INavigationService<SettingsViewModel> settingsNavigationService;
public SharedState SharedState { get; }
public override string Title => "Settings";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
// ReSharper disable once MemberCanBeMadeStatic.Global
public string AppVersion =>
$"Version {Compat.AppVersion}" + (Program.IsDebugBuild ? " (Debug)" : "");
// Theme section
[ObservableProperty]
private string? selectedTheme;
public IReadOnlyList<string> AvailableThemes { get; } = new[] { "Light", "Dark", "System", };
[ObservableProperty]
private CultureInfo selectedLanguage;
// ReSharper disable once MemberCanBeMadeStatic.Global
public IReadOnlyList<CultureInfo> AvailableLanguages => Cultures.SupportedCultures;
public IReadOnlyList<float> AnimationScaleOptions { get; } =
new[] { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f, };
[ObservableProperty]
private float selectedAnimationScale;
// Shared folder options
[ObservableProperty]
private bool removeSymlinksOnShutdown;
// Inference UI section
[ObservableProperty]
private bool isPromptCompletionEnabled = true;
[ObservableProperty]
private IReadOnlyList<string> availableTagCompletionCsvs = Array.Empty<string>();
[ObservableProperty]
private string? selectedTagCompletionCsv;
[ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true;
[ObservableProperty]
[CustomValidation(typeof(MainSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
private string? outputImageFileNameFormat;
[ObservableProperty]
private string? outputImageFileNameFormatSample;
public IEnumerable<FileNameFormatVar> OutputImageFileNameFormatVars =>
FileNameFormatProvider
.GetSample()
.Substitutions.Select(
kv =>
new FileNameFormatVar
{
Variable = $"{{{kv.Key}}}",
Example = kv.Value.Invoke()
}
);
[ObservableProperty]
private bool isImageViewerPixelGridEnabled = true;
// Integrations section
[ObservableProperty]
private bool isDiscordRichPresenceEnabled;
// Debug section
[ObservableProperty]
private string? debugPaths;
[ObservableProperty]
private string? debugCompatInfo;
[ObservableProperty]
private string? debugGpuInfo;
// Info section
private const int VersionTapCountThreshold = 7;
[ObservableProperty, NotifyPropertyChangedFor(nameof(VersionFlyoutText))]
private int versionTapCount;
[ObservableProperty]
private bool isVersionTapTeachingTipOpen;
public string VersionFlyoutText =>
$"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options.";
public string DataDirectory =>
settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set";
public MainSettingsViewModel(
INotificationService notificationService,
ISettingsManager settingsManager,
IPrerequisiteHelper prerequisiteHelper,
IPyRunner pyRunner,
ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService,
SharedState sharedState,
ICompletionProvider completionProvider,
IModelIndexService modelIndexService,
INavigationService<SettingsViewModel> settingsNavigationService
)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.prerequisiteHelper = prerequisiteHelper;
this.pyRunner = pyRunner;
this.dialogFactory = dialogFactory;
this.trackedDownloadService = trackedDownloadService;
this.completionProvider = completionProvider;
this.modelIndexService = modelIndexService;
this.settingsNavigationService = settingsNavigationService;
SharedState = sharedState;
SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1];
SelectedLanguage = Cultures.GetSupportedCultureOrDefault(settingsManager.Settings.Language);
RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown;
SelectedAnimationScale = settingsManager.Settings.AnimationScale;
settingsManager.RelayPropertyFor(this, vm => vm.SelectedTheme, settings => settings.Theme);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedTagCompletionCsv,
settings => settings.TagCompletionCsv
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsPromptCompletionEnabled,
settings => settings.IsPromptCompletionEnabled,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsCompletionRemoveUnderscoresEnabled,
settings => settings.IsCompletionRemoveUnderscoresEnabled,
true
);
this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(formatProperty =>
{
var provider = FileNameFormatProvider.GetSample();
var template = formatProperty.Value ?? string.Empty;
if (
!string.IsNullOrEmpty(template)
&& provider.Validate(template) == ValidationResult.Success
)
{
var format = FileNameFormat.Parse(template, provider);
OutputImageFileNameFormatSample = format.GetFileName() + ".png";
}
else
{
// Use default format if empty
var defaultFormat = FileNameFormat.Parse(
FileNameFormat.DefaultTemplate,
provider
);
OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png";
}
});
settingsManager.RelayPropertyFor(
this,
vm => vm.OutputImageFileNameFormat,
settings => settings.InferenceOutputImageFileNameFormat,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsImageViewerPixelGridEnabled,
settings => settings.IsImageViewerPixelGridEnabled,
true
);
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(
notificationService,
LogLevel.Warn
);
ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
}
/// <inheritdoc />
public override async Task OnLoadedAsync()
{
await base.OnLoadedAsync();
await notificationService.TryAsync(completionProvider.Setup());
UpdateAvailableTagCompletionCsvs();
}
public static ValidationResult ValidateOutputImageFileNameFormat(
string? format,
ValidationContext context
)
{
return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty);
}
partial void OnSelectedThemeChanged(string? value)
{
// In case design / tests
if (Application.Current is null)
return;
// Change theme
Application.Current.RequestedThemeVariant = value switch
{
"Dark" => ThemeVariant.Dark,
"Light" => ThemeVariant.Light,
_ => ThemeVariant.Default
};
}
partial void OnSelectedLanguageChanged(CultureInfo? oldValue, CultureInfo newValue)
{
if (oldValue is null || newValue.Name == Cultures.Current?.Name)
return;
// Set locale
if (AvailableLanguages.Contains(newValue))
{
Logger.Info("Changing language from {Old} to {New}", oldValue, newValue);
Cultures.TrySetSupportedCulture(newValue);
settingsManager.Transaction(s => s.Language = newValue.Name);
var dialog = new BetterContentDialog
{
Title = Resources.Label_RelaunchRequired,
Content = Resources.Text_RelaunchRequiredToApplyLanguage,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = Resources.Action_Relaunch,
CloseButtonText = Resources.Action_RelaunchLater
};
Dispatcher.UIThread.InvokeAsync(async () =>
{
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
Process.Start(Compat.AppCurrentPath);
App.Shutdown();
}
});
}
else
{
Logger.Info(
"Requested invalid language change from {Old} to {New}",
oldValue,
newValue
);
}
}
partial void OnRemoveSymlinksOnShutdownChanged(bool value)
{
settingsManager.Transaction(s => s.RemoveFolderLinksOnShutdown = value);
}
public async Task ResetCheckpointCache()
{
settingsManager.Transaction(s => s.InstalledModelHashes = new HashSet<string>());
await Task.Run(() => settingsManager.IndexCheckpoints());
notificationService.Show(
"Checkpoint cache reset",
"The checkpoint cache has been reset.",
NotificationType.Success
);
}
[RelayCommand]
private void NavigateToInferenceSettings()
{
Dispatcher.UIThread.Post(
() =>
settingsNavigationService.NavigateTo<InferenceSettingsViewModel>(
new BetterSlideNavigationTransition
{
Effect = SlideNavigationTransitionEffect.FromRight
}
)
);
}
#region Package Environment
[RelayCommand]
private async Task OpenEnvVarsDialog()
{
var viewModel = dialogFactory.Get<EnvVarsViewModel>();
// Load current settings
var current =
settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>(
current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value))
);
var dialog = new BetterContentDialog
{
Content = new EnvVarsDialog { DataContext = viewModel },
PrimaryButtonText = Resources.Action_Save,
IsPrimaryButtonEnabled = true,
CloseButtonText = Resources.Action_Cancel,
};
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
// Save settings
var newEnvVars = viewModel.EnvVars
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key))
.GroupBy(kvp => kvp.Key, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First().Value, StringComparer.Ordinal);
settingsManager.Transaction(s => s.EnvironmentVariables = newEnvVars);
}
}
[RelayCommand]
private async Task CheckPythonVersion()
{
var isInstalled = prerequisiteHelper.IsPythonInstalled;
Logger.Debug($"Check python installed: {isInstalled}");
// Ensure python installed
if (!prerequisiteHelper.IsPythonInstalled)
{
// Need 7z as well for site packages repack
Logger.Debug("Python not installed, unpacking resources...");
await prerequisiteHelper.UnpackResourcesIfNecessary();
Logger.Debug("Unpacked resources, installing python...");
await prerequisiteHelper.InstallPythonIfNecessary();
}
// Get python version
await pyRunner.Initialize();
var result = await pyRunner.GetVersionInfo();
// Show dialog box
var dialog = new ContentDialog
{
Title = Resources.Label_PythonVersionInfo,
Content = result,
PrimaryButtonText = Resources.Action_OK,
IsPrimaryButtonEnabled = true
};
await dialog.ShowAsync();
}
#endregion
#region Inference UI
private void UpdateAvailableTagCompletionCsvs()
{
if (!settingsManager.IsLibraryDirSet)
return;
var tagsDir = settingsManager.TagsDirectory;
if (!tagsDir.Exists)
return;
var csvFiles = tagsDir.Info.EnumerateFiles("*.csv");
AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray();
// Set selected to current if exists
var settingsCsv = settingsManager.Settings.TagCompletionCsv;
if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv))
{
SelectedTagCompletionCsv = settingsCsv;
}
}
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task ImportTagCsv()
{
var storage = App.StorageProvider;
var files = await storage.OpenFilePickerAsync(
new FilePickerOpenOptions
{
FileTypeFilter = new List<FilePickerFileType>
{
new("CSV") { Patterns = new[] { "*.csv" }, }
}
}
);
if (files.Count == 0)
return;
var sourceFile = new FilePath(files[0].TryGetLocalPath()!);
var tagsDir = settingsManager.TagsDirectory;
tagsDir.Create();
// Copy to tags directory
var targetFile = tagsDir.JoinFile(sourceFile.Name);
await sourceFile.CopyToAsync(targetFile);
// Update index
UpdateAvailableTagCompletionCsvs();
// Trigger load
completionProvider.BackgroundLoadFromFile(targetFile, true);
notificationService.Show(
$"Imported {sourceFile.Name}",
$"The {sourceFile.Name} file has been imported.",
NotificationType.Success
);
}
#endregion
#region System
/// <summary>
/// Adds Stability Matrix to Start Menu for the current user.
/// </summary>
[RelayCommand]
private async Task AddToStartMenu()
{
if (!Compat.IsWindows)
{
notificationService.Show("Not supported", "This feature is only supported on Windows.");
return;
}
await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
"Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
var iconPath = shortcutDir.JoinFile("Stability Matrix.ico");
await Assets.AppIcon.ExtractTo(iconPath);
WindowsShortcuts.CreateShortcut(shortcutLink, appPath, iconPath, "Stability Matrix");
notificationService.Show(
"Added to Start Menu",
"Stability Matrix has been added to the Start Menu.",
NotificationType.Success
);
}
/// <summary>
/// Add Stability Matrix to Start Menu for all users.
/// <remarks>Requires Admin elevation.</remarks>
/// </summary>
[RelayCommand]
private async Task AddToGlobalStartMenu()
{
if (!Compat.IsWindows)
{
notificationService.Show("Not supported", "This feature is only supported on Windows.");
return;
}
// Confirmation dialog
var dialog = new BetterContentDialog
{
Title =
"This will create a shortcut for Stability Matrix in the Start Menu for all users",
Content = "You will be prompted for administrator privileges. Continue?",
PrimaryButtonText = Resources.Action_Yes,
CloseButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary
};
if (await dialog.ShowAsync() != ContentDialogResult.Primary)
{
return;
}
await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
var iconPath = shortcutDir.JoinFile("Stability Matrix.ico");
// We can't directly write to the targets, so extract to temporary directory first
using var tempDir = new TempDirectoryPath();
await Assets.AppIcon.ExtractTo(tempDir.JoinFile("Stability Matrix.ico"));
WindowsShortcuts.CreateShortcut(
tempDir.JoinFile("Stability Matrix.lnk"),
appPath,
iconPath,
"Stability Matrix"
);
// Move to target
try
{
var moveLinkResult = await WindowsElevated.MoveFiles(
(tempDir.JoinFile("Stability Matrix.lnk"), shortcutLink),
(tempDir.JoinFile("Stability Matrix.ico"), iconPath)
);
if (moveLinkResult != 0)
{
notificationService.ShowPersistent(
"Failed to create shortcut",
$"Could not copy shortcut",
NotificationType.Error
);
}
}
catch (Win32Exception e)
{
// We'll get this exception if user cancels UAC
Logger.Warn(e, "Could not create shortcut");
notificationService.Show("Could not create shortcut", "", NotificationType.Warning);
return;
}
notificationService.Show(
"Added to Start Menu",
"Stability Matrix has been added to the Start Menu for all users.",
NotificationType.Success
);
}
public async Task PickNewDataDirectory()
{
var viewModel = dialogFactory.Get<SelectDataDirectoryViewModel>();
var dialog = new BetterContentDialog
{
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new SelectDataDirectoryDialog { DataContext = viewModel }
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
// 1. For portable mode, call settings.SetPortableMode()
if (viewModel.IsPortableMode)
{
settingsManager.SetPortableMode();
}
// 2. For custom path, call settings.SetLibraryPath(path)
else
{
settingsManager.SetLibraryPath(viewModel.DataDirectory);
}
// Restart
var restartDialog = new BetterContentDialog
{
Title = "Restart required",
Content = "Stability Matrix must be restarted for the changes to take effect.",
PrimaryButtonText = Resources.Action_Restart,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = false,
};
await restartDialog.ShowAsync();
Process.Start(Compat.AppCurrentPath);
App.Shutdown();
}
}
#endregion
#region Debug Section
public void LoadDebugInfo()
{
var assembly = Assembly.GetExecutingAssembly();
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
DebugPaths = $"""
Current Working Directory [Environment.CurrentDirectory]
"{Environment.CurrentDirectory}"
App Directory [Assembly.GetExecutingAssembly().Location]
"{assembly.Location}"
App Directory [AppContext.BaseDirectory]
"{AppContext.BaseDirectory}"
AppData Directory [SpecialFolder.ApplicationData]
"{appData}"
""";
// 1. Check portable mode
var appDir = Compat.AppCurrentDir;
var expectedPortableFile = Path.Combine(appDir, "Data", ".sm-portable");
var isPortableMode = File.Exists(expectedPortableFile);
DebugCompatInfo = $"""
Platform: {Compat.Platform}
AppData: {Compat.AppData}
AppDataHome: {Compat.AppDataHome}
AppCurrentDir: {Compat.AppCurrentDir}
ExecutableName: {Compat.GetExecutableName()}
-- Settings --
Expected Portable Marker file: {expectedPortableFile}
Portable Marker file exists: {isPortableMode}
IsLibraryDirSet = {settingsManager.IsLibraryDirSet}
IsPortableMode = {settingsManager.IsPortableMode}
""";
// Get Gpu info
var gpuInfo = "";
foreach (var (i, gpu) in HardwareHelper.IterGpuInfo().Enumerate())
{
gpuInfo += $"[{i + 1}] {gpu}\n";
}
DebugGpuInfo = gpuInfo;
}
// Debug buttons
[RelayCommand]
private void DebugNotification()
{
notificationService.Show(
new Notification(
title: "Test Notification",
message: "Here is some message",
type: NotificationType.Information
)
);
}
[RelayCommand]
private async Task DebugContentDialog()
{
var dialog = new ContentDialog
{
DefaultButton = ContentDialogButton.Primary,
Title = "Test title",
PrimaryButtonText = Resources.Action_OK,
CloseButtonText = Resources.Action_Close
};
var result = await dialog.ShowAsync();
notificationService.Show(new Notification("Content dialog closed", $"Result: {result}"));
}
[RelayCommand]
private void DebugThrowException()
{
throw new OperationCanceledException("Example Message");
}
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task DebugThrowAsyncException()
{
await Task.Yield();
throw new ApplicationException("Example Message");
}
[RelayCommand]
private async Task DebugMakeImageGrid()
{
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(
new FilePickerOpenOptions() { AllowMultiple = true }
);
if (files.Count == 0)
return;
var images = await files.SelectAsync(
async f => SKImage.FromEncodedData(await f.OpenReadAsync())
);
var grid = ImageProcessor.CreateImageGrid(images.ToImmutableArray());
// Show preview
using var peekPixels = grid.PeekPixels();
using var data = peekPixels.Encode(SKEncodedImageFormat.Jpeg, 100);
await using var stream = data.AsStream();
var bitmap = WriteableBitmap.Decode(stream);
var galleryImages = new List<ImageSource> { new(bitmap), };
galleryImages.AddRange(files.Select(f => new ImageSource(f.Path.ToString())));
var imageBox = new ImageGalleryCard
{
Width = 1000,
Height = 900,
DataContext = dialogFactory.Get<ImageGalleryCardViewModel>(vm =>
{
vm.ImageSources.AddRange(galleryImages);
})
};
var dialog = new BetterContentDialog
{
MaxDialogWidth = 1000,
MaxDialogHeight = 1000,
FullSizeDesired = true,
Content = imageBox,
CloseButtonText = "Close",
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
};
await dialog.ShowAsync();
}
[RelayCommand]
private async Task DebugLoadCompletionCsv()
{
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions());
if (files.Count == 0)
return;
await completionProvider.LoadFromFile(files[0].TryGetLocalPath()!, true);
notificationService.Show("Loaded completion file", "");
}
[RelayCommand]
private async Task DebugImageMetadata()
{
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions());
if (files.Count == 0)
return;
var metadata = ImageMetadata.ParseFile(files[0].TryGetLocalPath()!);
var textualTags = metadata.GetTextualData()?.ToArray();
if (textualTags is null)
{
notificationService.Show("No textual data found", "");
return;
}
if (metadata.GetGenerationParameters() is { } parameters)
{
var parametersJson = JsonSerializer.Serialize(parameters);
var dialog = DialogHelper.CreateJsonDialog(parametersJson, "Generation Parameters");
await dialog.ShowAsync();
}
}
[RelayCommand]
private async Task DebugRefreshModelsIndex()
{
await modelIndexService.RefreshIndex();
}
[RelayCommand]
private async Task DebugTrackedDownload()
{
var textFields = new TextBoxField[]
{
new() { Label = "Url", },
new() { Label = "File path" }
};
var dialog = DialogHelper.CreateTextEntryDialog("Add download", "", textFields);
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
var url = textFields[0].Text;
var filePath = textFields[1].Text;
var download = trackedDownloadService.NewDownload(new Uri(url), new FilePath(filePath));
download.Start();
}
}
#endregion
#region Info Section
public void OnVersionClick()
{
// Ignore if already enabled
if (SharedState.IsDebugMode)
return;
VersionTapCount++;
switch (VersionTapCount)
{
// Reached required threshold
case >= VersionTapCountThreshold:
{
IsVersionTapTeachingTipOpen = false;
// Enable debug options
SharedState.IsDebugMode = true;
notificationService.Show(
"Debug options enabled",
"Warning: Improper use may corrupt application state or cause loss of data."
);
VersionTapCount = 0;
break;
}
// Open teaching tip above 3rd click
case >= 3:
IsVersionTapTeachingTipOpen = true;
break;
}
}
[RelayCommand]
private async Task ShowLicensesDialog()
{
try
{
var markdown = GetLicensesMarkdown();
var dialog = DialogHelper.CreateMarkdownDialog(markdown, "Licenses");
dialog.MaxDialogHeight = 600;
await dialog.ShowAsync();
}
catch (Exception e)
{
notificationService.Show(
"Failed to read licenses information",
$"{e}",
NotificationType.Error
);
}
}
private static string GetLicensesMarkdown()
{
// Read licenses.json
using var reader = new StreamReader(Assets.LicensesJson.Open());
var licenses =
JsonSerializer.Deserialize<IReadOnlyList<LicenseInfo>>(reader.ReadToEnd())
?? throw new InvalidOperationException("Failed to read licenses.json");
// Generate markdown
var builder = new StringBuilder();
foreach (var license in licenses)
{
builder.AppendLine(
$"## [{license.PackageName}]({license.PackageUrl}) by {string.Join(", ", license.Authors)}"
);
builder.AppendLine();
builder.AppendLine(license.Description);
builder.AppendLine();
builder.AppendLine($"[{license.LicenseUrl}]({license.LicenseUrl})");
builder.AppendLine();
}
return builder.ToString();
}
#endregion
}

901
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -1,51 +1,16 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using NLog;
using SkiaSharp;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -57,875 +22,47 @@ public partial class SettingsViewModel : PageViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly IPrerequisiteHelper prerequisiteHelper;
private readonly IPyRunner pyRunner;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ICompletionProvider completionProvider;
private readonly ITrackedDownloadService trackedDownloadService;
private readonly IModelIndexService modelIndexService;
public SharedState SharedState { get; }
public override string Title => "Settings";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
// ReSharper disable once MemberCanBeMadeStatic.Global
public string AppVersion =>
$"Version {Compat.AppVersion}" + (Program.IsDebugBuild ? " (Debug)" : "");
// Theme section
[ObservableProperty]
private string? selectedTheme;
public IReadOnlyList<string> AvailableThemes { get; } = new[] { "Light", "Dark", "System", };
[ObservableProperty]
private CultureInfo selectedLanguage;
// ReSharper disable once MemberCanBeMadeStatic.Global
public IReadOnlyList<CultureInfo> AvailableLanguages => Cultures.SupportedCultures;
public IReadOnlyList<float> AnimationScaleOptions { get; } =
new[] { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f, };
[ObservableProperty]
private float selectedAnimationScale;
// Shared folder options
[ObservableProperty]
private bool removeSymlinksOnShutdown;
// Inference UI section
[ObservableProperty]
private bool isPromptCompletionEnabled = true;
[ObservableProperty]
private IReadOnlyList<string> availableTagCompletionCsvs = Array.Empty<string>();
[ObservableProperty]
private string? selectedTagCompletionCsv;
[ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true;
[ObservableProperty]
[CustomValidation(typeof(SettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
private string? outputImageFileNameFormat;
[ObservableProperty]
private string? outputImageFileNameFormatSample;
public IEnumerable<FileNameFormatVar> OutputImageFileNameFormatVars =>
FileNameFormatProvider
.GetSample()
.Substitutions.Select(
kv =>
new FileNameFormatVar
{
Variable = $"{{{kv.Key}}}",
Example = kv.Value.Invoke()
}
);
[ObservableProperty]
private bool isImageViewerPixelGridEnabled = true;
// Integrations section
[ObservableProperty]
private bool isDiscordRichPresenceEnabled;
// Debug section
[ObservableProperty]
private string? debugPaths;
[ObservableProperty]
private string? debugCompatInfo;
public IReadOnlyList<PageViewModelBase> SubPages { get; }
[ObservableProperty]
private string? debugGpuInfo;
// Info section
private const int VersionTapCountThreshold = 7;
[ObservableProperty, NotifyPropertyChangedFor(nameof(VersionFlyoutText))]
private int versionTapCount;
private ObservableCollection<PageViewModelBase> currentPagePath = new();
[ObservableProperty]
private bool isVersionTapTeachingTipOpen;
public string VersionFlyoutText =>
$"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options.";
public string DataDirectory =>
settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set";
public SettingsViewModel(
INotificationService notificationService,
ISettingsManager settingsManager,
IPrerequisiteHelper prerequisiteHelper,
IPyRunner pyRunner,
ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService,
SharedState sharedState,
ICompletionProvider completionProvider,
IModelIndexService modelIndexService
)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.prerequisiteHelper = prerequisiteHelper;
this.pyRunner = pyRunner;
this.dialogFactory = dialogFactory;
this.trackedDownloadService = trackedDownloadService;
this.completionProvider = completionProvider;
this.modelIndexService = modelIndexService;
SharedState = sharedState;
SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1];
SelectedLanguage = Cultures.GetSupportedCultureOrDefault(settingsManager.Settings.Language);
RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown;
SelectedAnimationScale = settingsManager.Settings.AnimationScale;
settingsManager.RelayPropertyFor(this, vm => vm.SelectedTheme, settings => settings.Theme);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedTagCompletionCsv,
settings => settings.TagCompletionCsv
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsPromptCompletionEnabled,
settings => settings.IsPromptCompletionEnabled,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsCompletionRemoveUnderscoresEnabled,
settings => settings.IsCompletionRemoveUnderscoresEnabled,
true
);
this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(formatProperty =>
{
var provider = FileNameFormatProvider.GetSample();
var template = formatProperty.Value ?? string.Empty;
if (
!string.IsNullOrEmpty(template)
&& provider.Validate(template) == ValidationResult.Success
)
{
var format = FileNameFormat.Parse(template, provider);
OutputImageFileNameFormatSample = format.GetFileName() + ".png";
}
else
{
// Use default format if empty
var defaultFormat = FileNameFormat.Parse(
FileNameFormat.DefaultTemplate,
provider
);
OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png";
}
});
settingsManager.RelayPropertyFor(
this,
vm => vm.OutputImageFileNameFormat,
settings => settings.InferenceOutputImageFileNameFormat,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsImageViewerPixelGridEnabled,
settings => settings.IsImageViewerPixelGridEnabled,
true
);
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(
notificationService,
LogLevel.Warn
);
ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
}
/// <inheritdoc />
public override async Task OnLoadedAsync()
{
await base.OnLoadedAsync();
private PageViewModelBase? currentPage;
await notificationService.TryAsync(completionProvider.Setup());
UpdateAvailableTagCompletionCsvs();
}
public static ValidationResult ValidateOutputImageFileNameFormat(
string? format,
ValidationContext context
)
public SettingsViewModel(ServiceManager<ViewModelBase> vmFactory)
{
return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty);
}
partial void OnSelectedThemeChanged(string? value)
SubPages = new PageViewModelBase[]
{
// In case design / tests
if (Application.Current is null)
return;
// Change theme
Application.Current.RequestedThemeVariant = value switch
{
"Dark" => ThemeVariant.Dark,
"Light" => ThemeVariant.Light,
_ => ThemeVariant.Default
vmFactory.Get<MainSettingsViewModel>(),
vmFactory.Get<InferenceSettingsViewModel>(),
};
}
partial void OnSelectedLanguageChanged(CultureInfo? oldValue, CultureInfo newValue)
{
if (oldValue is null || newValue.Name == Cultures.Current?.Name)
return;
// Set locale
if (AvailableLanguages.Contains(newValue))
{
Logger.Info("Changing language from {Old} to {New}", oldValue, newValue);
Cultures.TrySetSupportedCulture(newValue);
settingsManager.Transaction(s => s.Language = newValue.Name);
var dialog = new BetterContentDialog
{
Title = Resources.Label_RelaunchRequired,
Content = Resources.Text_RelaunchRequiredToApplyLanguage,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = Resources.Action_Relaunch,
CloseButtonText = Resources.Action_RelaunchLater
};
Dispatcher.UIThread.InvokeAsync(async () =>
{
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
Process.Start(Compat.AppCurrentPath);
App.Shutdown();
}
});
}
else
{
Logger.Info(
"Requested invalid language change from {Old} to {New}",
oldValue,
newValue
);
}
}
partial void OnRemoveSymlinksOnShutdownChanged(bool value)
{
settingsManager.Transaction(s => s.RemoveFolderLinksOnShutdown = value);
}
public async Task ResetCheckpointCache()
{
settingsManager.Transaction(s => s.InstalledModelHashes = new HashSet<string>());
await Task.Run(() => settingsManager.IndexCheckpoints());
notificationService.Show(
"Checkpoint cache reset",
"The checkpoint cache has been reset.",
NotificationType.Success
);
}
#region Package Environment
[RelayCommand]
private async Task OpenEnvVarsDialog()
{
var viewModel = dialogFactory.Get<EnvVarsViewModel>();
// Load current settings
var current =
settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>(
current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value))
);
var dialog = new BetterContentDialog
{
Content = new EnvVarsDialog { DataContext = viewModel },
PrimaryButtonText = Resources.Action_Save,
IsPrimaryButtonEnabled = true,
CloseButtonText = Resources.Action_Cancel,
};
CurrentPagePath.AddRange(SubPages);
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
// Save settings
var newEnvVars = viewModel.EnvVars
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key))
.GroupBy(kvp => kvp.Key, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First().Value, StringComparer.Ordinal);
settingsManager.Transaction(s => s.EnvironmentVariables = newEnvVars);
}
CurrentPage = SubPages[0];
}
[RelayCommand]
private async Task CheckPythonVersion()
partial void OnCurrentPageChanged(PageViewModelBase? value)
{
var isInstalled = prerequisiteHelper.IsPythonInstalled;
Logger.Debug($"Check python installed: {isInstalled}");
// Ensure python installed
if (!prerequisiteHelper.IsPythonInstalled)
if (value is null)
{
// Need 7z as well for site packages repack
Logger.Debug("Python not installed, unpacking resources...");
await prerequisiteHelper.UnpackResourcesIfNecessary();
Logger.Debug("Unpacked resources, installing python...");
await prerequisiteHelper.InstallPythonIfNecessary();
}
// Get python version
await pyRunner.Initialize();
var result = await pyRunner.GetVersionInfo();
// Show dialog box
var dialog = new ContentDialog
{
Title = Resources.Label_PythonVersionInfo,
Content = result,
PrimaryButtonText = Resources.Action_OK,
IsPrimaryButtonEnabled = true
};
await dialog.ShowAsync();
}
#endregion
#region Inference UI
private void UpdateAvailableTagCompletionCsvs()
{
if (!settingsManager.IsLibraryDirSet)
return;
var tagsDir = settingsManager.TagsDirectory;
if (!tagsDir.Exists)
return;
var csvFiles = tagsDir.Info.EnumerateFiles("*.csv");
AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray();
// Set selected to current if exists
var settingsCsv = settingsManager.Settings.TagCompletionCsv;
if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv))
{
SelectedTagCompletionCsv = settingsCsv;
}
}
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task ImportTagCsv()
{
var storage = App.StorageProvider;
var files = await storage.OpenFilePickerAsync(
new FilePickerOpenOptions
{
FileTypeFilter = new List<FilePickerFileType>
if (value is MainSettingsViewModel)
{
new("CSV") { Patterns = new[] { "*.csv" }, }
CurrentPagePath.Clear();
CurrentPagePath.Add(value);
}
}
);
if (files.Count == 0)
return;
var sourceFile = new FilePath(files[0].TryGetLocalPath()!);
var tagsDir = settingsManager.TagsDirectory;
tagsDir.Create();
// Copy to tags directory
var targetFile = tagsDir.JoinFile(sourceFile.Name);
await sourceFile.CopyToAsync(targetFile);
// Update index
UpdateAvailableTagCompletionCsvs();
// Trigger load
completionProvider.BackgroundLoadFromFile(targetFile, true);
notificationService.Show(
$"Imported {sourceFile.Name}",
$"The {sourceFile.Name} file has been imported.",
NotificationType.Success
);
}
#endregion
#region System
/// <summary>
/// Adds Stability Matrix to Start Menu for the current user.
/// </summary>
[RelayCommand]
private async Task AddToStartMenu()
{
if (!Compat.IsWindows)
{
notificationService.Show("Not supported", "This feature is only supported on Windows.");
return;
}
await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
"Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
var iconPath = shortcutDir.JoinFile("Stability Matrix.ico");
await Assets.AppIcon.ExtractTo(iconPath);
WindowsShortcuts.CreateShortcut(shortcutLink, appPath, iconPath, "Stability Matrix");
notificationService.Show(
"Added to Start Menu",
"Stability Matrix has been added to the Start Menu.",
NotificationType.Success
);
}
/// <summary>
/// Add Stability Matrix to Start Menu for all users.
/// <remarks>Requires Admin elevation.</remarks>
/// </summary>
[RelayCommand]
private async Task AddToGlobalStartMenu()
{
if (!Compat.IsWindows)
{
notificationService.Show("Not supported", "This feature is only supported on Windows.");
return;
}
// Confirmation dialog
var dialog = new BetterContentDialog
{
Title =
"This will create a shortcut for Stability Matrix in the Start Menu for all users",
Content = "You will be prompted for administrator privileges. Continue?",
PrimaryButtonText = Resources.Action_Yes,
CloseButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary
};
if (await dialog.ShowAsync() != ContentDialogResult.Primary)
{
return;
}
await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
var iconPath = shortcutDir.JoinFile("Stability Matrix.ico");
// We can't directly write to the targets, so extract to temporary directory first
using var tempDir = new TempDirectoryPath();
await Assets.AppIcon.ExtractTo(tempDir.JoinFile("Stability Matrix.ico"));
WindowsShortcuts.CreateShortcut(
tempDir.JoinFile("Stability Matrix.lnk"),
appPath,
iconPath,
"Stability Matrix"
);
// Move to target
try
{
var moveLinkResult = await WindowsElevated.MoveFiles(
(tempDir.JoinFile("Stability Matrix.lnk"), shortcutLink),
(tempDir.JoinFile("Stability Matrix.ico"), iconPath)
);
if (moveLinkResult != 0)
{
notificationService.ShowPersistent(
"Failed to create shortcut",
$"Could not copy shortcut",
NotificationType.Error
);
}
}
catch (Win32Exception e)
{
// We'll get this exception if user cancels UAC
Logger.Warn(e, "Could not create shortcut");
notificationService.Show("Could not create shortcut", "", NotificationType.Warning);
return;
}
notificationService.Show(
"Added to Start Menu",
"Stability Matrix has been added to the Start Menu for all users.",
NotificationType.Success
);
}
public async Task PickNewDataDirectory()
{
var viewModel = dialogFactory.Get<SelectDataDirectoryViewModel>();
var dialog = new BetterContentDialog
{
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new SelectDataDirectoryDialog { DataContext = viewModel }
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
// 1. For portable mode, call settings.SetPortableMode()
if (viewModel.IsPortableMode)
{
settingsManager.SetPortableMode();
}
// 2. For custom path, call settings.SetLibraryPath(path)
else
{
settingsManager.SetLibraryPath(viewModel.DataDirectory);
}
// Restart
var restartDialog = new BetterContentDialog
{
Title = "Restart required",
Content = "Stability Matrix must be restarted for the changes to take effect.",
PrimaryButtonText = Resources.Action_Restart,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = false,
};
await restartDialog.ShowAsync();
Process.Start(Compat.AppCurrentPath);
App.Shutdown();
}
}
#endregion
#region Debug Section
public void LoadDebugInfo()
{
var assembly = Assembly.GetExecutingAssembly();
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
DebugPaths = $"""
Current Working Directory [Environment.CurrentDirectory]
"{Environment.CurrentDirectory}"
App Directory [Assembly.GetExecutingAssembly().Location]
"{assembly.Location}"
App Directory [AppContext.BaseDirectory]
"{AppContext.BaseDirectory}"
AppData Directory [SpecialFolder.ApplicationData]
"{appData}"
""";
// 1. Check portable mode
var appDir = Compat.AppCurrentDir;
var expectedPortableFile = Path.Combine(appDir, "Data", ".sm-portable");
var isPortableMode = File.Exists(expectedPortableFile);
DebugCompatInfo = $"""
Platform: {Compat.Platform}
AppData: {Compat.AppData}
AppDataHome: {Compat.AppDataHome}
AppCurrentDir: {Compat.AppCurrentDir}
ExecutableName: {Compat.GetExecutableName()}
-- Settings --
Expected Portable Marker file: {expectedPortableFile}
Portable Marker file exists: {isPortableMode}
IsLibraryDirSet = {settingsManager.IsLibraryDirSet}
IsPortableMode = {settingsManager.IsPortableMode}
""";
// Get Gpu info
var gpuInfo = "";
foreach (var (i, gpu) in HardwareHelper.IterGpuInfo().Enumerate())
{
gpuInfo += $"[{i + 1}] {gpu}\n";
}
DebugGpuInfo = gpuInfo;
}
// Debug buttons
[RelayCommand]
private void DebugNotification()
{
notificationService.Show(
new Notification(
title: "Test Notification",
message: "Here is some message",
type: NotificationType.Information
)
);
}
[RelayCommand]
private async Task DebugContentDialog()
{
var dialog = new ContentDialog
{
DefaultButton = ContentDialogButton.Primary,
Title = "Test title",
PrimaryButtonText = Resources.Action_OK,
CloseButtonText = Resources.Action_Close
};
var result = await dialog.ShowAsync();
notificationService.Show(new Notification("Content dialog closed", $"Result: {result}"));
}
[RelayCommand]
private void DebugThrowException()
{
throw new OperationCanceledException("Example Message");
}
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task DebugThrowAsyncException()
{
await Task.Yield();
throw new ApplicationException("Example Message");
}
[RelayCommand]
private async Task DebugMakeImageGrid()
{
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(
new FilePickerOpenOptions() { AllowMultiple = true }
);
if (files.Count == 0)
return;
var images = await files.SelectAsync(
async f => SKImage.FromEncodedData(await f.OpenReadAsync())
);
var grid = ImageProcessor.CreateImageGrid(images.ToImmutableArray());
// Show preview
using var peekPixels = grid.PeekPixels();
using var data = peekPixels.Encode(SKEncodedImageFormat.Jpeg, 100);
await using var stream = data.AsStream();
var bitmap = WriteableBitmap.Decode(stream);
var galleryImages = new List<ImageSource> { new(bitmap), };
galleryImages.AddRange(files.Select(f => new ImageSource(f.Path.ToString())));
var imageBox = new ImageGalleryCard
{
Width = 1000,
Height = 900,
DataContext = dialogFactory.Get<ImageGalleryCardViewModel>(vm =>
{
vm.ImageSources.AddRange(galleryImages);
})
};
var dialog = new BetterContentDialog
{
MaxDialogWidth = 1000,
MaxDialogHeight = 1000,
FullSizeDesired = true,
Content = imageBox,
CloseButtonText = "Close",
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
};
await dialog.ShowAsync();
}
[RelayCommand]
private async Task DebugLoadCompletionCsv()
{
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions());
if (files.Count == 0)
return;
await completionProvider.LoadFromFile(files[0].TryGetLocalPath()!, true);
notificationService.Show("Loaded completion file", "");
}
[RelayCommand]
private async Task DebugImageMetadata()
{
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions());
if (files.Count == 0)
return;
var metadata = ImageMetadata.ParseFile(files[0].TryGetLocalPath()!);
var textualTags = metadata.GetTextualData()?.ToArray();
if (textualTags is null)
{
notificationService.Show("No textual data found", "");
return;
}
if (metadata.GetGenerationParameters() is { } parameters)
{
var parametersJson = JsonSerializer.Serialize(parameters);
var dialog = DialogHelper.CreateJsonDialog(parametersJson, "Generation Parameters");
await dialog.ShowAsync();
CurrentPagePath.Clear();
CurrentPagePath.AddRange(new[] { SubPages[0], value });
}
}
[RelayCommand]
private async Task DebugRefreshModelsIndex()
{
await modelIndexService.RefreshIndex();
}
[RelayCommand]
private async Task DebugTrackedDownload()
{
var textFields = new TextBoxField[]
{
new() { Label = "Url", },
new() { Label = "File path" }
};
var dialog = DialogHelper.CreateTextEntryDialog("Add download", "", textFields);
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
var url = textFields[0].Text;
var filePath = textFields[1].Text;
var download = trackedDownloadService.NewDownload(new Uri(url), new FilePath(filePath));
download.Start();
}
}
#endregion
#region Info Section
public void OnVersionClick()
{
// Ignore if already enabled
if (SharedState.IsDebugMode)
return;
VersionTapCount++;
switch (VersionTapCount)
{
// Reached required threshold
case >= VersionTapCountThreshold:
{
IsVersionTapTeachingTipOpen = false;
// Enable debug options
SharedState.IsDebugMode = true;
notificationService.Show(
"Debug options enabled",
"Warning: Improper use may corrupt application state or cause loss of data."
);
VersionTapCount = 0;
break;
}
// Open teaching tip above 3rd click
case >= 3:
IsVersionTapTeachingTipOpen = true;
break;
}
}
[RelayCommand]
private async Task ShowLicensesDialog()
{
try
{
var markdown = GetLicensesMarkdown();
var dialog = DialogHelper.CreateMarkdownDialog(markdown, "Licenses");
dialog.MaxDialogHeight = 600;
await dialog.ShowAsync();
}
catch (Exception e)
{
notificationService.Show(
"Failed to read licenses information",
$"{e}",
NotificationType.Error
);
}
}
private static string GetLicensesMarkdown()
{
// Read licenses.json
using var reader = new StreamReader(Assets.LicensesJson.Open());
var licenses =
JsonSerializer.Deserialize<IReadOnlyList<LicenseInfo>>(reader.ReadToEnd())
?? throw new InvalidOperationException("Failed to read licenses.json");
// Generate markdown
var builder = new StringBuilder();
foreach (var license in licenses)
{
builder.AppendLine(
$"## [{license.PackageName}]({license.PackageUrl}) by {string.Join(", ", license.Authors)}"
);
builder.AppendLine();
builder.AppendLine(license.Description);
builder.AppendLine();
builder.AppendLine($"[{license.LicenseUrl}]({license.LicenseUrl})");
builder.AppendLine();
}
return builder.ToString();
}
#endregion
}

18
StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
@ -24,8 +25,10 @@ using Microsoft.Extensions.DependencyInjection;
using NLog;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
@ -44,7 +47,7 @@ namespace StabilityMatrix.Avalonia.Views;
public partial class MainWindow : AppWindowBase
{
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
private readonly INavigationService<MainWindowViewModel> navigationService;
private FlyoutBase? progressFlyout;
@ -58,7 +61,7 @@ public partial class MainWindow : AppWindowBase
public MainWindow(
INotificationService notificationService,
INavigationService navigationService
INavigationService<MainWindowViewModel> navigationService
)
{
this.notificationService = notificationService;
@ -74,6 +77,8 @@ public partial class MainWindow : AppWindowBase
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
navigationService.TypedNavigation += NavigationService_OnTypedNavigation;
EventManager.Instance.ToggleProgressFlyout += (_, _) => progressFlyout?.Hide();
EventManager.Instance.CultureChanged += (_, _) => SetDefaultFonts();
EventManager.Instance.UpdateAvailable += OnUpdateAvailable;
@ -158,6 +163,15 @@ public partial class MainWindow : AppWindowBase
}
}
private void NavigationService_OnTypedNavigation(object? sender, TypedNavigationEventArgs e)
{
var mainViewModel = (MainWindowViewModel)DataContext!;
mainViewModel.SelectedCategory = mainViewModel.Pages.FirstOrDefault(
x => x.GetType() == e.ViewModelType
);
}
private void OnUpdateAvailable(object? sender, UpdateInfo? updateInfo)
{
Dispatcher.UIThread.Post(() =>

67
StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml

@ -1,16 +1,69 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.Settings.InferenceSettingsPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings"
d:DataContext="{x:Static mocks:DesignData.InferenceSettingsViewModel}"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="vmSettings:InferenceSettingsViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StabilityMatrix.Avalonia.Views.Settings.InferenceSettingsPage">
Welcome to Avalonia!
mc:Ignorable="d">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
<Grid RowDefinitions="Auto,*,*,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="Stuff" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Theme}"
IconSource="WeatherMoon">
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="100" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Language}"
IconSource="Character">
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="100" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="3"
Margin="8,0,8,4"
Header="Inference"
IconSource="Code" />
</Grid>
<!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*,Auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="Other stuff" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
IconSource="Folder">
<ui:SettingsExpander.Footer>
<CheckBox Margin="8" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
</StackPanel>
</ScrollViewer>
</controls:UserControlBase>

10
StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml.cs

@ -1,19 +1,13 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Settings;
[Singleton]
public partial class InferenceSettingsPage : UserControl
public partial class InferenceSettingsPage : UserControlBase
{
public InferenceSettingsPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

533
StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml

@ -0,0 +1,533 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.Settings.MainSettingsPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings"
Focusable="True"
d:DataContext="{x:Static mocks:DesignData.MainSettingsViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="vmSettings:MainSettingsViewModel"
mc:Ignorable="d">
<controls:UserControlBase.Resources>
<converters:CultureInfoDisplayConverter x:Key="CultureInfoDisplayConverter" />
</controls:UserControlBase.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
<Grid RowDefinitions="Auto,*,*,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_Appearance}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Theme}"
IconSource="WeatherMoon">
<ui:SettingsExpander.Footer>
<ComboBox
MinWidth="100"
ItemsSource="{Binding AvailableThemes}"
SelectedItem="{Binding SelectedTheme}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Language}"
IconSource="Character">
<ui:SettingsExpander.Footer>
<ComboBox
MinWidth="100"
DisplayMemberBinding="{Binding Converter={StaticResource CultureInfoDisplayConverter}}"
ItemsSource="{Binding AvailableLanguages}"
SelectedItem="{Binding SelectedLanguage}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="3"
IsVisible="{Binding SharedState.IsDebugMode}"
Margin="8,0,8,4"
IsClickEnabled="True"
Command="{Binding NavigateToInferenceSettingsCommand}"
Header="Inference (Test)"
IconSource="Code"
ActionIconSource="ChevronRight">
</ui:SettingsExpander>
</Grid>
<!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*,Auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_CheckpointManager}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
Description="{x:Static lang:Resources.Label_RemoveSymlinksOnShutdown_Details}"
Header="{x:Static lang:Resources.Label_RemoveSymlinksOnShutdown}"
IconSource="Folder">
<ui:SettingsExpander.Footer>
<CheckBox Margin="8" IsChecked="{Binding RemoveSymlinksOnShutdown}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,4"
Description="{x:Static lang:Resources.Label_ResetCheckpointsCache_Details}"
Header="{x:Static lang:Resources.Label_ResetCheckpointsCache}"
IconSource="Refresh">
<ui:SettingsExpander.Footer>
<Button Command="{Binding ResetCheckpointCache}" Content="{x:Static lang:Resources.Label_ResetCheckpointsCache}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- Inference UI -->
<Grid Margin="0,8,0,0" RowDefinitions="auto,*,*,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="Inference" />
<!-- Auto Completion -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="Prompt Auto Completion">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-wand-magic-sparkles" />
</ui:SettingsExpander.IconSource>
<!-- Enable toggle -->
<ui:SettingsExpanderItem Content="Enable">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding IsPromptCompletionEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv selection -->
<ui:SettingsExpanderItem
Content="Tag Source"
Description="Tags to use for completion in .csv format (Compatible with a1111-sd-webui-tagcomplete)"
IconSource="Tag"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox ItemsSource="{Binding AvailableTagCompletionCsvs}" SelectedItem="{Binding SelectedTagCompletionCsv}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv import -->
<ui:SettingsExpanderItem
Content="Import Tag Source .csv"
IconSource="Add"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding ImportTagCsvCommand}" Content="Import" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Remove underscores -->
<ui:SettingsExpanderItem
Content="Replace underscores with spaces when inserting completions"
IconSource="Underline"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsCompletionRemoveUnderscoresEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Image Viewer -->
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="Image Viewer"
IconSource="Image">
<!-- Pixel grid -->
<ui:SettingsExpanderItem Content="Show pixel grid at high zoom levels" IconSource="ViewAll">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsImageViewerPixelGridEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Output Image Files -->
<ui:SettingsExpander
Grid.Row="3"
Margin="8,0,8,4"
Header="Output Image Files">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="TabDesktopImage" />
</ui:SettingsExpander.IconSource>
<!-- File name pattern -->
<ui:SettingsExpanderItem
Content="File name pattern"
Description="{Binding OutputImageFileNameFormatSample}"
IconSource="Rename">
<ui:SettingsExpanderItem.Footer>
<TextBox
Name="OutputImageFileNameFormatTextBox"
MinWidth="150"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
FontSize="13"
Text="{Binding OutputImageFileNameFormat}"
Watermark="{x:Static inference:FileNameFormat.DefaultTemplate}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:TeachingTip
Title="Format Variables"
Grid.Row="3"
IsOpen="{Binding #OutputImageFileNameFormatTextBox.IsFocused}"
PreferredPlacement="Top"
Target="{Binding #OutputImageFileNameFormatTextBox, Mode=OneWay}">
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding OutputImageFileNameFormatVars}" />
<!--<mdxaml:MarkdownScrollViewer
Markdown="{Binding OutputImageFileNameFormatGuideMarkdown}"/>-->
</ui:TeachingTip>
</Grid>
<!-- Environment Options -->
<Grid RowDefinitions="Auto, Auto, Auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_PackageEnvironment}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
Header="{x:Static lang:Resources.Label_EnvironmentVariables}"
IconSource="OtherUser">
<ui:SettingsExpander.Footer>
<Button Command="{Binding OpenEnvVarsDialogCommand}" Content="{x:Static lang:Resources.Action_Edit}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,4"
Header="{x:Static lang:Resources.Label_EmbeddedPython}">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-python" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="16">
<controls:ProgressRing
BorderThickness="3"
IsEnabled="{Binding CheckPythonVersionCommand.IsRunning}"
IsIndeterminate="True"
IsVisible="{Binding CheckPythonVersionCommand.IsRunning}" />
<Button Command="{Binding CheckPythonVersionCommand}" Content="{x:Static lang:Resources.Action_CheckVersion}" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- Integrations -->
<Grid RowDefinitions="auto,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_Integrations}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_DiscordRichPresence}">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-discord" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding IsDiscordRichPresenceEnabled}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- System Options -->
<Grid RowDefinitions="auto, auto, auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_System}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Description="{x:Static lang:Resources.Label_AddToStartMenu_Details}"
Header="{x:Static lang:Resources.Label_AddToStartMenu}"
IconSource="StarAdd"
ToolTip.Tip="{OnPlatform Default={x:Static lang:Resources.Label_OnlyAvailableOnWindows},
Windows={x:Null}}">
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="8">
<controls:ProgressRing
BorderThickness="3"
IsEnabled="{Binding IsVisible, RelativeSource={RelativeSource Self}}"
IsIndeterminate="True">
<controls:ProgressRing.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.Or}">
<Binding Path="AddToStartMenuCommand.IsRunning" />
<Binding Path="AddToGlobalStartMenuCommand.IsRunning" />
</MultiBinding>
</controls:ProgressRing.IsVisible>
</controls:ProgressRing>
<SplitButton
Command="{Binding AddToStartMenuCommand}"
Content="{x:Static lang:Resources.Action_AddForCurrentUser}"
IsEnabled="{OnPlatform Default=False,
Windows=True}">
<SplitButton.Flyout>
<ui:FAMenuFlyout Placement="Bottom">
<ui:MenuFlyoutItem
Command="{Binding AddToGlobalStartMenuCommand}"
IconSource="Admin"
Text="{x:Static lang:Resources.Action_AddForAllUsers}" />
</ui:FAMenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0"
Description="{x:Static lang:Resources.Label_SelectNewDataDirectory_Details}"
IconSource="MoveToFolder">
<ui:SettingsExpander.Header>
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Static lang:Resources.Label_SelectNewDataDirectory}" />
<TextBlock FontSize="12" Foreground="{DynamicResource TextFillColorSecondaryBrush}">
<Run Text="{x:Static lang:Resources.Label_CurrentDirectory}" />
<Run Text="{Binding DataDirectory}" />
</TextBlock>
</StackPanel>
</ui:SettingsExpander.Header>
<ui:SettingsExpander.Footer>
<Button Command="{Binding PickNewDataDirectory}">
<Grid ColumnDefinitions="Auto, Auto">
<avalonia:Icon
Grid.Row="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
Value="fa-solid fa-folder-open" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Action_SelectDirectory}" />
</Grid>
</Button>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- Debug Options -->
<Grid IsVisible="{Binding SharedState.IsDebugMode}" RowDefinitions="auto,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="Debug Options" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,0"
Command="{Binding LoadDebugInfo}"
Header="Debug Options"
IconSource="Code">
<ui:SettingsExpanderItem
Margin="4,0"
Description="Paths"
IconSource="Folder">
<SelectableTextBlock
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding DebugPaths}"
TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Description="Compat Info"
IconSource="StarFilled">
<SelectableTextBlock
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding DebugCompatInfo}"
TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Description="GPU Info"
IconSource="FullScreenMaximize">
<SelectableTextBlock
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding DebugGpuInfo}"
TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Content="Animation Scale"
Description="Lower values = faster animations. 0x means animations are instant."
IconSource="Clock">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding AnimationScaleOptions}" SelectedItem="{Binding SelectedAnimationScale}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding}" /><Run Text="x" />
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Content="Notification"
IconSource="CommentAdd">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding DebugNotificationCommand}" Content="New Notification" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Content="Content Dialog"
IconSource="NewWindow">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding DebugContentDialogCommand}" Content="Show Dialog" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Content="Exceptions"
IconSource="Flag">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding DebugThrowExceptionCommand}" Content="Unhandled Exception" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0,4,4"
Content="Download Manager tests"
IconSource="Flag">
<ui:SettingsExpanderItem.Footer>
<Button
Margin="0,8"
Command="{Binding DebugTrackedDownloadCommand}"
Content="Add Tracked Download" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0,4,4"
Content="Refresh Models Index"
IconSource="SyncFolder">
<ui:SettingsExpanderItem.Footer>
<Button
Margin="0,8"
Command="{Binding DebugRefreshModelsIndexCommand}"
Content="Refresh Index" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0,4,4"
Content="Make image grid"
IconSource="Image">
<ui:SettingsExpanderItem.Footer>
<Button
Margin="0,8"
Command="{Binding DebugMakeImageGridCommand}"
Content="Select images" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0,4,4"
Content="Image metadata parser"
IconSource="Flag">
<ui:SettingsExpanderItem.Footer>
<Button
Margin="0,8"
Command="{Binding DebugImageMetadataCommand}"
Content="Choose image" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Grid>
<!-- TODO: Directories card -->
<Grid RowDefinitions="auto,*">
<StackPanel
Grid.Row="1"
HorizontalAlignment="Left"
Orientation="Vertical">
<TextBlock
Margin="0,8"
FontSize="15"
FontWeight="Bold"
Text="{x:Static lang:Resources.Label_About}" />
<Image
Width="112"
Height="112"
Margin="8"
HorizontalAlignment="Left"
Source="/Assets/Icon.png" />
<TextBlock
Margin="8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_StabilityMatrix}" />
<Panel>
<Button
Name="VersionButton"
Margin="8,0,8,8"
Padding="2,0,2,0"
BorderThickness="0"
Classes="transparent"
Command="{Binding OnVersionClick}"
Content="{Binding AppVersion}" />
<ui:TeachingTip
Title="{Binding VersionFlyoutText}"
IsOpen="{Binding IsVersionTapTeachingTipOpen}"
PreferredPlacement="RightTop"
Target="{Binding #VersionButton}" />
</Panel>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Button
Margin="8"
HorizontalAlignment="Left"
Command="{Binding ShowLicensesDialogCommand}"
Content="{x:Static lang:Resources.Label_LicenseAndOpenSourceNotices}" />
</StackPanel>
</StackPanel>
</Grid>
<!-- Extra space at the bottom -->
<Panel Margin="0,0,0,16" />
</StackPanel>
</ScrollViewer>
</controls:UserControlBase>

13
StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml.cs

@ -0,0 +1,13 @@
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views.Settings;
[Singleton]
public partial class MainSettingsPage : UserControlBase
{
public MainSettingsPage()
{
InitializeComponent();
}
}

525
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

@ -2,19 +2,14 @@
x:Class="StabilityMatrix.Avalonia.Views.SettingsPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:vmBase="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base"
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
Focusable="True"
d:DataContext="{x:Static mocks:DesignData.SettingsViewModel}"
d:DesignHeight="700"
@ -24,513 +19,35 @@
mc:Ignorable="d">
<controls:UserControlBase.Resources>
<converters:CultureInfoDisplayConverter x:Key="CultureInfoDisplayConverter" />
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">24</x:Double>
</controls:UserControlBase.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
<Grid RowDefinitions="Auto,*,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_Appearance}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Theme}"
IconSource="WeatherMoon">
<ui:SettingsExpander.Footer>
<ComboBox
MinWidth="100"
ItemsSource="{Binding AvailableThemes}"
SelectedItem="{Binding SelectedTheme}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Language}"
IconSource="Character">
<ui:SettingsExpander.Footer>
<ComboBox
MinWidth="100"
ItemsSource="{Binding AvailableLanguages}"
DisplayMemberBinding="{Binding Converter={StaticResource CultureInfoDisplayConverter}}"
SelectedItem="{Binding SelectedLanguage}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*,Auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_CheckpointManager}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
Description="{x:Static lang:Resources.Label_RemoveSymlinksOnShutdown_Details}"
Header="{x:Static lang:Resources.Label_RemoveSymlinksOnShutdown}"
IconSource="Folder">
<ui:SettingsExpander.Footer>
<CheckBox Margin="8" IsChecked="{Binding RemoveSymlinksOnShutdown}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,4"
Description="{x:Static lang:Resources.Label_ResetCheckpointsCache_Details}"
Header="{x:Static lang:Resources.Label_ResetCheckpointsCache}"
IconSource="Refresh">
<ui:SettingsExpander.Footer>
<Button
Command="{Binding ResetCheckpointCache}"
Content="{x:Static lang:Resources.Label_ResetCheckpointsCache}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- Inference UI -->
<Grid Margin="0,8,0,0" RowDefinitions="auto,*,*,*">
<TextBlock
FontWeight="Medium"
Text="Inference"
Margin="0,0,0,8" />
<!-- Auto Completion -->
<ui:SettingsExpander Grid.Row="1"
Header="Prompt Auto Completion"
Margin="8,0,8,4">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-wand-magic-sparkles"/>
</ui:SettingsExpander.IconSource>
<!-- Enable toggle -->
<ui:SettingsExpanderItem Content="Enable">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch
IsChecked="{Binding IsPromptCompletionEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv selection -->
<ui:SettingsExpanderItem Content="Tag Source"
IconSource="Tag"
IsEnabled="{Binding IsPromptCompletionEnabled}"
Description="Tags to use for completion in .csv format (Compatible with a1111-sd-webui-tagcomplete)">
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox
ItemsSource="{Binding AvailableTagCompletionCsvs}"
SelectedItem="{Binding SelectedTagCompletionCsv}"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv import -->
<ui:SettingsExpanderItem Content="Import Tag Source .csv"
IconSource="Add"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<Grid RowDefinitions="Auto,*">
<Button
Command="{Binding ImportTagCsvCommand}"
Content="Import"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Remove underscores -->
<ui:SettingsExpanderItem
Content="Replace underscores with spaces when inserting completions"
IconSource="Underline"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8"
IsChecked="{Binding IsCompletionRemoveUnderscoresEnabled}"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
Grid.Row="1">
<!-- Image Viewer -->
<ui:SettingsExpander Grid.Row="2"
Header="Image Viewer"
IconSource="Image"
Margin="8,0,8,4">
<!-- Pixel grid -->
<ui:SettingsExpanderItem
Content="Show pixel grid at high zoom levels"
IconSource="ViewAll">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8"
IsChecked="{Binding IsImageViewerPixelGridEnabled}"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Output Image Files -->
<ui:SettingsExpander Grid.Row="3"
Header="Output Image Files"
Margin="8,0,8,4">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="TabDesktopImage"/>
</ui:SettingsExpander.IconSource>
<!-- File name pattern -->
<ui:SettingsExpanderItem
Content="File name pattern"
Description="{Binding OutputImageFileNameFormatSample}"
IconSource="Rename">
<ui:SettingsExpanderItem.Footer>
<TextBox
Name="OutputImageFileNameFormatTextBox"
Watermark="{x:Static inference:FileNameFormat.DefaultTemplate}"
FontSize="13"
MinWidth="150"
Text="{Binding OutputImageFileNameFormat}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:TeachingTip
IsOpen="{Binding #OutputImageFileNameFormatTextBox.IsFocused}"
Target="{Binding #OutputImageFileNameFormatTextBox, Mode=OneWay}"
PreferredPlacement="Top"
Title="Format Variables"
Grid.Row="3">
<DataGrid
AutoGenerateColumns="True"
ItemsSource="{Binding OutputImageFileNameFormatVars}" />
<!--<mdxaml:MarkdownScrollViewer
Markdown="{Binding OutputImageFileNameFormatGuideMarkdown}"/>-->
</ui:TeachingTip>
</Grid>
<!-- Environment Options -->
<Grid RowDefinitions="Auto, Auto, Auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_PackageEnvironment}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
Header="{x:Static lang:Resources.Label_EnvironmentVariables}"
IconSource="OtherUser">
<ui:SettingsExpander.Footer>
<Button
Command="{Binding OpenEnvVarsDialogCommand}"
Content="{x:Static lang:Resources.Action_Edit}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,4"
Header="{x:Static lang:Resources.Label_EmbeddedPython}">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-python" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="16">
<controls:ProgressRing
BorderThickness="3"
IsEnabled="{Binding CheckPythonVersionCommand.IsRunning}"
IsIndeterminate="True"
IsVisible="{Binding CheckPythonVersionCommand.IsRunning}" />
<Button Command="{Binding CheckPythonVersionCommand}" Content="{x:Static lang:Resources.Action_CheckVersion}" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- Integrations -->
<Grid RowDefinitions="auto,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_Integrations}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_DiscordRichPresence}">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-discord" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding IsDiscordRichPresenceEnabled}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- System Options -->
<Grid RowDefinitions="auto, auto, auto">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_System}" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Description="{x:Static lang:Resources.Label_AddToStartMenu_Details}"
Header="{x:Static lang:Resources.Label_AddToStartMenu}"
IconSource="StarAdd"
ToolTip.Tip="{OnPlatform Default={x:Static lang:Resources.Label_OnlyAvailableOnWindows}, Windows={x:Null}}">
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="8">
<controls:ProgressRing
BorderThickness="3"
IsEnabled="{Binding IsVisible, RelativeSource={RelativeSource Self}}"
IsIndeterminate="True">
<controls:ProgressRing.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.Or}">
<Binding Path="AddToStartMenuCommand.IsRunning" />
<Binding Path="AddToGlobalStartMenuCommand.IsRunning" />
</MultiBinding>
</controls:ProgressRing.IsVisible>
</controls:ProgressRing>
<SplitButton
Command="{Binding AddToStartMenuCommand}"
Content="{x:Static lang:Resources.Action_AddForCurrentUser}"
IsEnabled="{OnPlatform Default=False, Windows=True}">
<SplitButton.Flyout>
<ui:FAMenuFlyout Placement="Bottom">
<ui:MenuFlyoutItem
Command="{Binding AddToGlobalStartMenuCommand}"
IconSource="Admin"
Text="{x:Static lang:Resources.Action_AddForAllUsers}" />
</ui:FAMenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0"
Description="{x:Static lang:Resources.Label_SelectNewDataDirectory_Details}"
IconSource="MoveToFolder">
<ui:SettingsExpander.Header>
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Static lang:Resources.Label_SelectNewDataDirectory}"/>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
FontSize="12">
<Run Text="{x:Static lang:Resources.Label_CurrentDirectory}"/>
<Run Text="{Binding DataDirectory}"/>
</TextBlock>
</StackPanel>
</ui:SettingsExpander.Header>
<ui:SettingsExpander.Footer>
<Button Command="{Binding PickNewDataDirectory}">
<Grid ColumnDefinitions="Auto, Auto">
<avalonia:Icon
Grid.Row="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
Value="fa-solid fa-folder-open" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Action_SelectDirectory}" />
</Grid>
</Button>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Grid>
<!-- Debug Options -->
<Grid
IsVisible="{Binding SharedState.IsDebugMode}"
RowDefinitions="auto,*">
<TextBlock
Margin="0,0,0,8"
FontWeight="Medium"
Text="Debug Options" />
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,0"
Command="{Binding LoadDebugInfo}"
Header="Debug Options"
IconSource="Code">
<ui:SettingsExpanderItem
Margin="4,0"
Description="Paths"
IconSource="Folder">
<SelectableTextBlock
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding DebugPaths}"
TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Description="Compat Info"
IconSource="StarFilled">
<SelectableTextBlock
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding DebugCompatInfo}"
TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Description="GPU Info"
IconSource="FullScreenMaximize">
<SelectableTextBlock
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding DebugGpuInfo}"
TextWrapping="WrapWithOverflow" />
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Content="Animation Scale"
Description="Lower values = faster animations. 0x means animations are instant."
IconSource="Clock">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding AnimationScaleOptions}" SelectedItem="{Binding SelectedAnimationScale}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding}" /><Run Text="x" />
</TextBlock>
<ui:BreadcrumbBar
Grid.Row="0"
Margin="16,8"
x:Name="BreadcrumbBar"
ItemsSource="{Binding CurrentPagePath}">
<ui:BreadcrumbBar.ItemTemplate>
<DataTemplate x:DataType="vmBase:PageViewModelBase">
<ui:BreadcrumbBarItem Content="{Binding Title}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Content="Notification"
IconSource="CommentAdd">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding DebugNotificationCommand}" Content="New Notification" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Content="Content Dialog"
IconSource="NewWindow">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding DebugContentDialogCommand}" Content="Show Dialog" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0"
Content="Exceptions"
IconSource="Flag">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding DebugThrowExceptionCommand}" Content="Unhandled Exception" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0,4,4"
Content="Download Manager tests"
IconSource="Flag">
<ui:SettingsExpanderItem.Footer>
<Button
Margin="0,8"
Command="{Binding DebugTrackedDownloadCommand}"
Content="Add Tracked Download" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:BreadcrumbBar.ItemTemplate>
</ui:BreadcrumbBar>
<ui:SettingsExpanderItem
Margin="4,0,4,4"
Content="Refresh Models Index"
IconSource="SyncFolder">
<ui:SettingsExpanderItem.Footer>
<Button
Margin="0,8"
Command="{Binding DebugRefreshModelsIndexCommand}"
Content="Refresh Index" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0,4,4"
Content="Make image grid"
IconSource="Image">
<ui:SettingsExpanderItem.Footer>
<Button
Margin="0,8"
Command="{Binding DebugMakeImageGridCommand}"
Content="Select images" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem
Margin="4,0,4,4"
Content="Image metadata parser"
IconSource="Flag">
<ui:SettingsExpanderItem.Footer>
<Button
Margin="0,8"
Command="{Binding DebugImageMetadataCommand}"
Content="Choose image" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Grid>
<!-- TODO: Directories card -->
<Grid RowDefinitions="auto,*">
<StackPanel
<ui:Frame
Grid.Row="1"
HorizontalAlignment="Left"
Orientation="Vertical">
<TextBlock
Margin="0,8"
FontSize="15"
FontWeight="Bold"
Text="{x:Static lang:Resources.Label_About}" />
<Image
Width="112"
Height="112"
Margin="8"
HorizontalAlignment="Left"
Source="/Assets/Icon.png" />
<TextBlock
Margin="8"
FontWeight="Medium"
Text="{x:Static lang:Resources.Label_StabilityMatrix}" />
<Panel>
<Button
Name="VersionButton"
Margin="8,0,8,8"
Padding="2,0,2,0"
BorderThickness="0"
Classes="transparent"
Command="{Binding OnVersionClick}"
Content="{Binding AppVersion}" />
<ui:TeachingTip
Title="{Binding VersionFlyoutText}"
IsOpen="{Binding IsVersionTapTeachingTipOpen}"
PreferredPlacement="RightTop"
Target="{Binding #VersionButton}" />
</Panel>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Button
Margin="8"
HorizontalAlignment="Left"
Command="{Binding ShowLicensesDialogCommand}"
Content="{x:Static lang:Resources.Label_LicenseAndOpenSourceNotices}" />
</StackPanel>
</StackPanel>
Name="FrameView">
<ui:Frame.NavigationPageFactory>
<local:ViewLocator/>
</ui:Frame.NavigationPageFactory>
</ui:Frame>
</Grid>
<!-- Extra space at the bottom -->
<Panel Margin="0,0,0,16" />
</StackPanel>
</ScrollViewer>
</controls:UserControlBase>

84
StabilityMatrix.Avalonia/Views/SettingsPage.axaml.cs

@ -1,5 +1,19 @@
using Avalonia.Markup.Xaml;
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Navigation;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Views;
@ -7,13 +21,77 @@ namespace StabilityMatrix.Avalonia.Views;
[Singleton]
public partial class SettingsPage : UserControlBase
{
private readonly INavigationService<SettingsViewModel> settingsNavigationService;
private SettingsViewModel ViewModel => (SettingsViewModel)DataContext!;
[DesignOnly(true)]
[Obsolete("For XAML use only", true)]
public SettingsPage()
: this(App.Services.GetRequiredService<INavigationService<SettingsViewModel>>()) { }
public SettingsPage(INavigationService<SettingsViewModel> settingsNavigationService)
{
this.settingsNavigationService = settingsNavigationService;
InitializeComponent();
settingsNavigationService.SetFrame(FrameView);
settingsNavigationService.TypedNavigation += NavigationService_OnTypedNavigation;
FrameView.Navigated += FrameView_Navigated;
BreadcrumbBar.ItemClicked += BreadcrumbBar_ItemClicked;
}
private void InitializeComponent()
/// <inheritdoc />
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
Dispatcher.UIThread.Post(
() =>
settingsNavigationService.NavigateTo(
ViewModel.SubPages[0],
new BetterSlideNavigationTransition
{
AvaloniaXamlLoader.Load(this);
Effect = SlideNavigationTransitionEffect.FromBottom
}
)
);
}
private void NavigationService_OnTypedNavigation(object? sender, TypedNavigationEventArgs e)
{
ViewModel.CurrentPage = ViewModel.SubPages.FirstOrDefault(
x => x.GetType() == e.ViewModelType
);
}
private async void FrameView_Navigated(object? sender, NavigationEventArgs args)
{
if (args.Content is not PageViewModelBase vm)
{
return;
}
ViewModel.CurrentPage = vm;
}
private async void BreadcrumbBar_ItemClicked(
BreadcrumbBar sender,
BreadcrumbBarItemClickedEventArgs args
)
{
if (args.Item is not PageViewModelBase viewModel)
{
return;
}
settingsNavigationService.NavigateTo(
viewModel,
new BetterSlideNavigationTransition
{
Effect = SlideNavigationTransitionEffect.FromLeft
}
);
}
}

11
StabilityMatrix.Core/Attributes/SingletonAttribute.cs

@ -3,16 +3,25 @@
namespace StabilityMatrix.Core.Attributes;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class SingletonAttribute : Attribute
{
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type? InterfaceType { get; init; }
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type? ImplType { get; init; }
public SingletonAttribute() { }
public SingletonAttribute(Type interfaceType)
{
InterfaceType = interfaceType;
}
public SingletonAttribute(Type interfaceType, Type implType)
{
InterfaceType = implType;
ImplType = implType;
}
}

Loading…
Cancel
Save