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.
337 lines
11 KiB
337 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 |
|
{ |
|
Title = Resources.Label_StabilityMatrixAlreadyRunning, |
|
Content = Resources.Label_AnotherInstanceAlreadyRunning, |
|
IsPrimaryButtonEnabled = true, |
|
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(); |
|
} |
|
}
|
|
|