Multi-Platform Package Manager for Stable Diffusion
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
11 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(MainWindow))]
public partial class MainWindowViewModel : ViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ITrackedDownloadService trackedDownloadService;
private readonly IDiscordRichPresenceService discordRichPresenceService;
private readonly IModelIndexService modelIndexService;
private readonly Lazy<IModelDownloadLinkHandler> modelDownloadLinkHandler;
public string Greeting => "Welcome to Avalonia!";
[ObservableProperty]
private PageViewModelBase? currentPage;
[ObservableProperty]
private object? selectedCategory;
[ObservableProperty]
private List<PageViewModelBase> pages = new();
[ObservableProperty]
private List<PageViewModelBase> footerPages = new();
public ProgressManagerViewModel ProgressManagerViewModel { get; init; }
public UpdateViewModel UpdateViewModel { get; init; }
public double PaneWidth =>
Cultures.Current switch
{
{ Name: "it-IT" } => 250,
{ Name: "fr-FR" } => 250,
{ Name: "es" } => 250,
{ Name: "ru-RU" } => 250,
{ Name: "tr-TR" } => 235,
{ Name: "de" } => 250,
{ Name: "pt-PT" } => 300,
_ => 200
};
public MainWindowViewModel(
ISettingsManager settingsManager,
IDiscordRichPresenceService discordRichPresenceService,
ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService,
Lazy<IModelDownloadLinkHandler> modelDownloadLinkHandler
)
{
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.discordRichPresenceService = discordRichPresenceService;
this.trackedDownloadService = trackedDownloadService;
this.modelIndexService = modelIndexService;
this.modelDownloadLinkHandler = modelDownloadLinkHandler;
ProgressManagerViewModel = dialogFactory.Get<ProgressManagerViewModel>();
UpdateViewModel = dialogFactory.Get<UpdateViewModel>();
}
public override void OnLoaded()
{
base.OnLoaded();
// Set only if null, since this may be called again when content dialogs open
CurrentPage ??= Pages.FirstOrDefault();
SelectedCategory ??= Pages.FirstOrDefault();
}
protected override async Task OnInitialLoadedAsync()
{
await base.OnLoadedAsync();
// Skip if design mode
if (Design.IsDesignMode)
return;
if (!await EnsureDataDirectory())
{
// False if user exited dialog, shutdown app
App.Shutdown();
return;
}
try
{
await modelDownloadLinkHandler.Value.StartListening();
}
catch (IOException)
{
var dialog = new BetterContentDialog
{
8 months ago
Title = Resources.Label_StabilityMatrixAlreadyRunning,
Content = Resources.Label_AnotherInstanceAlreadyRunning,
IsPrimaryButtonEnabled = true,
8 months ago
PrimaryButtonText = Resources.Action_Close,
DefaultButton = ContentDialogButton.Primary
};
await dialog.ShowAsync();
App.Shutdown();
return;
}
// Initialize Discord Rich Presence (this needs LibraryDir so is set here)
discordRichPresenceService.UpdateState();
// Load in-progress downloads
ProgressManagerViewModel.AddDownloads(trackedDownloadService.Downloads);
// Index checkpoints if we dont have
Task.Run(() => settingsManager.IndexCheckpoints()).SafeFireAndForget();
// Disable preload for now, might be causing https://github.com/LykosAI/StabilityMatrix/issues/249
/*if (!App.IsHeadlessMode)
{
PreloadPages();
}*/
Program.StartupTimer.Stop();
var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed);
Logger.Info($"App started ({startupTime})");
if (Program.Args.DebugOneClickInstall || settingsManager.Settings.InstalledPackages.Count == 0)
{
var viewModel = dialogFactory.Get<NewOneClickInstallViewModel>();
var dialog = new BetterContentDialog
{
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
FullSizeDesired = true,
MinDialogHeight = 775,
Content = new NewOneClickInstallDialog { DataContext = viewModel },
};
var firstDialogResult = await dialog.ShowAsync(App.TopLevel);
if (firstDialogResult != ContentDialogResult.Primary)
return;
var recommendedModelsViewModel = dialogFactory.Get<RecommendedModelsViewModel>();
dialog = new BetterContentDialog
{
IsPrimaryButtonEnabled = true,
FullSizeDesired = true,
MinDialogHeight = 900,
PrimaryButtonText = Resources.Action_Download,
CloseButtonText = Resources.Action_Close,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonCommand = recommendedModelsViewModel.DoImportCommand,
Content = new RecommendedModelsDialog { DataContext = recommendedModelsViewModel },
};
await dialog.ShowAsync(App.TopLevel);
EventManager.Instance.OnRecommendedModelsDialogClosed();
EventManager.Instance.OnDownloadsTeachingTipRequested();
}
}
private void PreloadPages()
{
// Preload pages with Preload attribute
foreach (
var page in Pages
.Concat(FooterPages)
.Where(p => p.GetType().GetCustomAttributes(typeof(PreloadAttribute), true).Any())
)
{
Dispatcher
.UIThread.InvokeAsync(
async () =>
{
var stopwatch = Stopwatch.StartNew();
// ReSharper disable once MethodHasAsyncOverload
page.OnLoaded();
await page.OnLoadedAsync();
// Get view
new ViewLocator().Build(page);
Logger.Trace(
$"Preloaded page {page.GetType().Name} in {stopwatch.Elapsed.TotalMilliseconds:F1}ms"
);
},
DispatcherPriority.Background
)
.ContinueWith(task =>
{
if (task.Exception is { } exception)
{
Logger.Error(exception, "Error preloading page");
Debug.Fail(exception.Message);
}
});
}
}
/// <summary>
/// Check if the data directory exists, if not, show the select data directory dialog.
/// </summary>
private async Task<bool> EnsureDataDirectory()
{
// If we can't find library, show selection dialog
var foundInitially = settingsManager.TryFindLibrary();
if (!foundInitially)
{
var result = await ShowSelectDataDirectoryDialog();
if (!result)
return false;
}
// Try to find library again, should be found now
if (!settingsManager.TryFindLibrary())
{
throw new Exception("Could not find library after setting path");
}
// Tell LaunchPage to load any packages if they selected an existing directory
if (!foundInitially)
{
EventManager.Instance.OnInstalledPackagesChanged();
}
// Check if there are old packages, if so show migration dialog
// TODO: Migration dialog
return true;
}
/// <summary>
/// Return true if we should show the update available teaching tip
/// </summary>
public bool ShouldShowUpdateAvailableTeachingTip([NotNullWhen(true)] UpdateInfo? info)
{
if (info is null)
{
return false;
}
// If matching settings seen version, don't show
if (info.Version == settingsManager.Settings.LastSeenUpdateVersion)
{
return false;
}
// Save that we have dismissed this update
settingsManager.Transaction(
s => s.LastSeenUpdateVersion = info.Version,
ignoreMissingLibraryDir: true
);
return true;
}
/// <summary>
/// Shows the select data directory dialog.
/// </summary>
/// <returns>true if path set successfully, false if user exited dialog.</returns>
private async Task<bool> ShowSelectDataDirectoryDialog()
{
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(App.TopLevel);
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);
}
// Indicate success
return true;
}
return false;
}
public async Task ShowUpdateDialog()
{
var viewModel = dialogFactory.Get<UpdateViewModel>();
var dialog = new BetterContentDialog
{
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
DefaultButton = ContentDialogButton.Close,
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new UpdateDialog { DataContext = viewModel }
};
await viewModel.Preload();
await dialog.ShowAsync();
}
}