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.
 
 
 

278 lines
9.0 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
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.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;
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 MainWindowViewModel(
ISettingsManager settingsManager,
IDiscordRichPresenceService discordRichPresenceService,
ServiceManager<ViewModelBase> dialogFactory,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService
)
{
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.discordRichPresenceService = discordRichPresenceService;
this.trackedDownloadService = trackedDownloadService;
this.modelIndexService = modelIndexService;
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();
}
public override async Task OnLoadedAsync()
{
await base.OnLoadedAsync();
// Skip if design mode
if (Design.IsDesignMode)
return;
if (!await EnsureDataDirectory())
{
// False if user exited dialog, shutdown app
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();
PreloadPages();
Program.StartupTimer.Stop();
var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed);
Logger.Info($"App started ({startupTime})");
if (Program.Args.DebugOneClickInstall || !settingsManager.Settings.InstalledPackages.Any())
{
var viewModel = dialogFactory.Get<OneClickInstallViewModel>();
var dialog = new BetterContentDialog
{
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new OneClickInstallDialog { DataContext = viewModel },
};
EventManager.Instance.OneClickInstallFinished += (_, skipped) =>
{
dialog.Hide();
if (skipped)
return;
EventManager.Instance.OnTeachingTooltipNeeded();
};
await dialog.ShowAsync();
}
}
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
if (!settingsManager.TryFindLibrary())
{
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
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();
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();
}
}