diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml
index f33a4d5c..d41fd95c 100644
--- a/StabilityMatrix.Avalonia/App.axaml
+++ b/StabilityMatrix.Avalonia/App.axaml
@@ -27,6 +27,8 @@
700
+ 32
+
avares://StabilityMatrix.Avalonia/Assets/Fonts/NotoSansJP#Noto Sans JP
@@ -39,6 +41,7 @@
+
diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs
index d15dc543..1bf930b5 100644
--- a/StabilityMatrix.Avalonia/App.axaml.cs
+++ b/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().ToArray()
+ }
+ );
foreach (var typePair in singletonTypes)
{
- if (typePair.Attribute.InterfaceType is null)
+ foreach (var attribute in typePair.Attributes)
{
- services.AddSingleton(typePair.Type);
- }
- else
- {
- services.AddSingleton(typePair.Attribute.InterfaceType, typePair.Type);
+ 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(attribute.InterfaceType, typePair.Type);
+ }
}
}
diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
index 4d785605..512548f7 100644
--- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs
+++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
@@ -103,7 +103,6 @@ public static class DesignData
// General services
services
.AddLogging()
- .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
@@ -437,6 +436,9 @@ public static class DesignData
public static InferenceSettingsViewModel InferenceSettingsViewModel =>
Services.GetRequiredService();
+ public static MainSettingsViewModel MainSettingsViewModel =>
+ Services.GetRequiredService();
+
public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService();
diff --git a/StabilityMatrix.Avalonia/Models/TypedNavigationEventArgs.cs b/StabilityMatrix.Avalonia/Models/TypedNavigationEventArgs.cs
new file mode 100644
index 00000000..121b456c
--- /dev/null
+++ b/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; }
+}
diff --git a/StabilityMatrix.Avalonia/Services/INavigationService.cs b/StabilityMatrix.Avalonia/Services/INavigationService.cs
index 4afa4d9f..8c8ee417 100644
--- a/StabilityMatrix.Avalonia/Services/INavigationService.cs
+++ b/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
{
+ event EventHandler? TypedNavigation;
+
///
/// Set the frame to use for navigation.
///
diff --git a/StabilityMatrix.Avalonia/Services/NavigationService.cs b/StabilityMatrix.Avalonia/Services/NavigationService.cs
index b9fb1952..c22a5eb4 100644
--- a/StabilityMatrix.Avalonia/Services/NavigationService.cs
+++ b/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),
+ InterfaceType = typeof(INavigationService)
+)]
+[Singleton(
+ ImplType = typeof(NavigationService),
+ InterfaceType = typeof(INavigationService)
+)]
+public class NavigationService : INavigationService
{
private Frame? _frame;
+ public event EventHandler? TypedNavigation;
+
///
public void SetFrame(Frame frame)
{
@@ -60,18 +70,10 @@ 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) }
+ );
}
///
@@ -106,5 +108,14 @@ public class NavigationService : INavigationService
TransitionInfoOverride = transitionInfo ?? new SuppressNavigationTransitionInfo()
}
);
+
+ TypedNavigation?.Invoke(
+ this,
+ new TypedNavigationEventArgs
+ {
+ ViewModelType = viewModel.GetType(),
+ ViewModel = viewModel
+ }
+ );
}
}
diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
index a0e6c8cc..4b29bdc7 100644
--- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
+++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
@@ -38,6 +38,7 @@
+
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
index bc1d60ff..63e5b131 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
+++ b/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();
+ var navService = App.Services.GetRequiredService>();
navService.NavigateTo(new SuppressNavigationTransitionInfo());
((IPersistentViewProvider)this).AttachedPersistentView = null;
navService.NavigateTo(new BetterEntranceNavigationTransition());
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
index 351f9a64..aad4cc87 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs
+++ b/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 navigationService;
private readonly IPackageFactory packageFactory;
[ObservableProperty]
@@ -56,7 +56,7 @@ public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBa
public InferenceConnectionHelpViewModel(
ISettingsManager settingsManager,
- INavigationService navigationService,
+ INavigationService navigationService,
IPackageFactory packageFactory
)
{
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
index a7d1d26d..718630a7 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
@@ -30,7 +30,7 @@ public partial class OneClickInstallViewModel : ContentDialogViewModelBase
private readonly IPrerequisiteHelper prerequisiteHelper;
private readonly ILogger logger;
private readonly IPyRunner pyRunner;
- private readonly INavigationService navigationService;
+ private readonly INavigationService navigationService;
private const string DefaultPackageName = "stable-diffusion-webui";
[ObservableProperty]
@@ -71,7 +71,7 @@ public partial class OneClickInstallViewModel : ContentDialogViewModelBase
IPrerequisiteHelper prerequisiteHelper,
ILogger logger,
IPyRunner pyRunner,
- INavigationService navigationService
+ INavigationService navigationService
)
{
this.settingsManager = settingsManager;
diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
index 8352f6d5..aa296d35 100644
--- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
+++ b/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 navigationService;
private readonly ILogger 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 navigationService,
ILogger logger
)
{
diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
index c9893634..182280de 100644
--- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
+++ b/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 navigationService;
private readonly ServiceManager vmFactory;
[ObservableProperty]
@@ -80,7 +80,7 @@ public partial class PackageCardViewModel : ProgressViewModel
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
- INavigationService navigationService,
+ INavigationService navigationService,
ServiceManager vmFactory
)
{
diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
index 80d5c7ba..4ce798cf 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
+++ b/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
+{
+ ///
+ public override string Title => "Inference";
+
+ ///
+ public override IconSource IconSource =>
+ new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
new file mode 100644
index 00000000..628ceb64
--- /dev/null
+++ b/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 dialogFactory;
+ private readonly ICompletionProvider completionProvider;
+ private readonly ITrackedDownloadService trackedDownloadService;
+ private readonly IModelIndexService modelIndexService;
+ private readonly INavigationService 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 AvailableThemes { get; } = new[] { "Light", "Dark", "System", };
+
+ [ObservableProperty]
+ private CultureInfo selectedLanguage;
+
+ // ReSharper disable once MemberCanBeMadeStatic.Global
+ public IReadOnlyList AvailableLanguages => Cultures.SupportedCultures;
+
+ public IReadOnlyList 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 availableTagCompletionCsvs = Array.Empty();
+
+ [ObservableProperty]
+ private string? selectedTagCompletionCsv;
+
+ [ObservableProperty]
+ private bool isCompletionRemoveUnderscoresEnabled = true;
+
+ [ObservableProperty]
+ [CustomValidation(typeof(MainSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
+ private string? outputImageFileNameFormat;
+
+ [ObservableProperty]
+ private string? outputImageFileNameFormatSample;
+
+ public IEnumerable 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 dialogFactory,
+ ITrackedDownloadService trackedDownloadService,
+ SharedState sharedState,
+ ICompletionProvider completionProvider,
+ IModelIndexService modelIndexService,
+ INavigationService 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);
+ }
+
+ ///
+ 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());
+ 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(
+ new BetterSlideNavigationTransition
+ {
+ Effect = SlideNavigationTransitionEffect.FromRight
+ }
+ )
+ );
+ }
+
+ #region Package Environment
+
+ [RelayCommand]
+ private async Task OpenEnvVarsDialog()
+ {
+ var viewModel = dialogFactory.Get();
+
+ // Load current settings
+ var current =
+ settingsManager.Settings.EnvironmentVariables ?? new Dictionary();
+ viewModel.EnvVars = new ObservableCollection(
+ 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
+ {
+ 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
+
+ ///
+ /// Adds Stability Matrix to Start Menu for the current user.
+ ///
+ [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
+ );
+ }
+
+ ///
+ /// Add Stability Matrix to Start Menu for all users.
+ /// Requires Admin elevation.
+ ///
+ [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();
+ 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 { new(bitmap), };
+ galleryImages.AddRange(files.Select(f => new ImageSource(f.Path.ToString())));
+
+ var imageBox = new ImageGalleryCard
+ {
+ Width = 1000,
+ Height = 900,
+ DataContext = dialogFactory.Get(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>(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
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
index 74dc145b..948960d3 100644
--- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
+++ b/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 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 AvailableThemes { get; } = new[] { "Light", "Dark", "System", };
-
- [ObservableProperty]
- private CultureInfo selectedLanguage;
-
- // ReSharper disable once MemberCanBeMadeStatic.Global
- public IReadOnlyList AvailableLanguages => Cultures.SupportedCultures;
-
- public IReadOnlyList 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 availableTagCompletionCsvs = Array.Empty();
+ public IReadOnlyList SubPages { get; }
[ObservableProperty]
- private string? selectedTagCompletionCsv;
+ private ObservableCollection currentPagePath = new();
[ObservableProperty]
- private bool isCompletionRemoveUnderscoresEnabled = true;
-
- [ObservableProperty]
- [CustomValidation(typeof(SettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
- private string? outputImageFileNameFormat;
-
- [ObservableProperty]
- private string? outputImageFileNameFormatSample;
-
- public IEnumerable 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 SettingsViewModel(
- INotificationService notificationService,
- ISettingsManager settingsManager,
- IPrerequisiteHelper prerequisiteHelper,
- IPyRunner pyRunner,
- ServiceManager 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);
- }
-
- ///
- 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());
- 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();
-
- // Load current settings
- var current =
- settingsManager.Settings.EnvironmentVariables ?? new Dictionary();
- viewModel.EnvVars = new ObservableCollection(
- 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
- {
- 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
+ private PageViewModelBase? currentPage;
- ///
- /// Adds Stability Matrix to Start Menu for the current user.
- ///
- [RelayCommand]
- private async Task AddToStartMenu()
+ public SettingsViewModel(ServiceManager vmFactory)
{
- if (!Compat.IsWindows)
+ SubPages = new PageViewModelBase[]
{
- 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
- );
- }
-
- ///
- /// Add Stability Matrix to Start Menu for all users.
- /// Requires Admin elevation.
- ///
- [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
+ vmFactory.Get(),
+ vmFactory.Get(),
};
- 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;
- }
+ CurrentPagePath.AddRange(SubPages);
- notificationService.Show(
- "Added to Start Menu",
- "Stability Matrix has been added to the Start Menu for all users.",
- NotificationType.Success
- );
+ CurrentPage = SubPages[0];
}
- public async Task PickNewDataDirectory()
+ partial void OnCurrentPageChanged(PageViewModelBase? value)
{
- var viewModel = dialogFactory.Get();
- var dialog = new BetterContentDialog
+ if (value is null)
{
- 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 { new(bitmap), };
- galleryImages.AddRange(files.Select(f => new ImageSource(f.Path.ToString())));
-
- var imageBox = new ImageGalleryCard
- {
- Width = 1000,
- Height = 900,
- DataContext = dialogFactory.Get(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)
+ if (value is MainSettingsViewModel)
{
- notificationService.Show(
- "Failed to read licenses information",
- $"{e}",
- NotificationType.Error
- );
+ CurrentPagePath.Clear();
+ CurrentPagePath.Add(value);
}
- }
-
- private static string GetLicensesMarkdown()
- {
- // Read licenses.json
- using var reader = new StreamReader(Assets.LicensesJson.Open());
- var licenses =
- JsonSerializer.Deserialize>(reader.ReadToEnd())
- ?? throw new InvalidOperationException("Failed to read licenses.json");
-
- // Generate markdown
- var builder = new StringBuilder();
- foreach (var license in licenses)
+ else
{
- 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();
+ CurrentPagePath.Clear();
+ CurrentPagePath.AddRange(new[] { SubPages[0], value });
}
-
- return builder.ToString();
}
-
- #endregion
}
diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
index 57fc1c04..ab02c19d 100644
--- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
+++ b/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 navigationService;
private FlyoutBase? progressFlyout;
@@ -58,7 +61,7 @@ public partial class MainWindow : AppWindowBase
public MainWindow(
INotificationService notificationService,
- INavigationService navigationService
+ INavigationService 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(() =>
diff --git a/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml
index 428c0483..a5abbfc3 100644
--- a/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml
@@ -1,16 +1,69 @@
-
- Welcome to Avalonia!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml.cs
index 20cf1dd1..eea11ded 100644
--- a/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml.cs
+++ b/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);
- }
}
diff --git a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
new file mode 100644
index 00000000..60c84b6c
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
@@ -0,0 +1,533 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml.cs
new file mode 100644
index 00000000..b06310a8
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml
index b3e47a2b..918411be 100644
--- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml
+++ b/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">
-
+ 24
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml.cs
index ba3d18c9..aa34012d 100644
--- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml.cs
+++ b/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 settingsNavigationService;
+
+ private SettingsViewModel ViewModel => (SettingsViewModel)DataContext!;
+
+ [DesignOnly(true)]
+ [Obsolete("For XAML use only", true)]
public SettingsPage()
+ : this(App.Services.GetRequiredService>()) { }
+
+ public SettingsPage(INavigationService settingsNavigationService)
{
+ this.settingsNavigationService = settingsNavigationService;
+
InitializeComponent();
+
+ settingsNavigationService.SetFrame(FrameView);
+ settingsNavigationService.TypedNavigation += NavigationService_OnTypedNavigation;
+ FrameView.Navigated += FrameView_Navigated;
+ BreadcrumbBar.ItemClicked += BreadcrumbBar_ItemClicked;
+ }
+
+ ///
+ protected override void OnLoaded(RoutedEventArgs e)
+ {
+ base.OnLoaded(e);
+
+ Dispatcher.UIThread.Post(
+ () =>
+ settingsNavigationService.NavigateTo(
+ ViewModel.SubPages[0],
+ new BetterSlideNavigationTransition
+ {
+ Effect = SlideNavigationTransitionEffect.FromBottom
+ }
+ )
+ );
}
- private void InitializeComponent()
+ private void NavigationService_OnTypedNavigation(object? sender, TypedNavigationEventArgs e)
{
- AvaloniaXamlLoader.Load(this);
+ 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
+ }
+ );
}
}
diff --git a/StabilityMatrix.Core/Attributes/SingletonAttribute.cs b/StabilityMatrix.Core/Attributes/SingletonAttribute.cs
index d6538a4e..39e1c8cd 100644
--- a/StabilityMatrix.Core/Attributes/SingletonAttribute.cs
+++ b/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;
+ }
}