using System; using System.Collections.Generic; using System.Diagnostics; 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.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 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 pages = new(); [ObservableProperty] private List footerPages = new(); public ProgressManagerViewModel ProgressManagerViewModel { get; init; } public UpdateViewModel UpdateViewModel { get; init; } public MainWindowViewModel( ISettingsManager settingsManager, IDiscordRichPresenceService discordRichPresenceService, ServiceManager dialogFactory, ITrackedDownloadService trackedDownloadService, IModelIndexService modelIndexService ) { this.settingsManager = settingsManager; this.dialogFactory = dialogFactory; this.discordRichPresenceService = discordRichPresenceService; this.trackedDownloadService = trackedDownloadService; this.modelIndexService = modelIndexService; ProgressManagerViewModel = dialogFactory.Get(); UpdateViewModel = dialogFactory.Get(); } 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(); if (!settingsManager.Settings.InstalledPackages.Any()) { var viewModel = dialogFactory.Get(); 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); } }); } } /// /// Check if the data directory exists, if not, show the select data directory dialog. /// private async Task 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; } /// /// Shows the select data directory dialog. /// /// true if path set successfully, false if user exited dialog. private async Task ShowSelectDataDirectoryDialog() { 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); } // Indicate success return true; } return false; } public async Task ShowUpdateDialog() { var viewModel = dialogFactory.Get(); var dialog = new BetterContentDialog { ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, DefaultButton = ContentDialogButton.Close, IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false, Content = new UpdateDialog { DataContext = viewModel } }; await dialog.ShowAsync(); } }