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.
245 lines
8.1 KiB
245 lines
8.1 KiB
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<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(); |
|
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> |
|
/// 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 dialog.ShowAsync(); |
|
} |
|
}
|
|
|