using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.PackageManager; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels; /// /// This is our ViewModel for the second page /// [View(typeof(PackageManagerPage))] [Singleton, ManagedService] public partial class PackageManagerViewModel : PageViewModelBase { private readonly ISettingsManager settingsManager; private readonly ServiceManager dialogFactory; private readonly INotificationService notificationService; private readonly INavigationService packageNavigationService; private readonly ILogger logger; private readonly RunningPackageService runningPackageService; public override string Title => Resources.Label_Packages; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true }; /// /// List of installed packages /// private readonly SourceCache installedPackages = new(p => p.Id); /// /// List of indexed packages without a corresponding installed package /// private readonly SourceCache unknownInstalledPackages = new(p => p.Id); public IObservableCollection Packages { get; } = new ObservableCollectionExtended(); public IObservableCollection PackageCards { get; } = new ObservableCollectionExtended(); private DispatcherTimer timer; public PackageManagerViewModel( ISettingsManager settingsManager, ServiceManager dialogFactory, INotificationService notificationService, INavigationService packageNavigationService, ILogger logger, RunningPackageService runningPackageService ) { this.settingsManager = settingsManager; this.dialogFactory = dialogFactory; this.notificationService = notificationService; this.packageNavigationService = packageNavigationService; this.logger = logger; this.runningPackageService = runningPackageService; EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged; EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished; var installed = installedPackages.Connect(); var unknown = unknownInstalledPackages.Connect(); installed .Or(unknown) .DeferUntilLoaded() .Bind(Packages) .Transform( p => dialogFactory.Get(vm => { vm.Package = p; vm.OnLoadedAsync().SafeFireAndForget(); }) ) .Bind(PackageCards) .Subscribe(); timer = new DispatcherTimer { Interval = TimeSpan.FromMinutes(60), IsEnabled = true }; timer.Tick += async (_, _) => await CheckPackagesForUpdates(); } private void OnOneClickInstallFinished(object? sender, bool e) { OnLoadedAsync().SafeFireAndForget(); } public void SetPackages(IEnumerable packages) { installedPackages.Edit(s => s.Load(packages)); } public void SetUnknownPackages(IEnumerable packages) { unknownInstalledPackages.Edit(s => s.Load(packages)); } protected override async Task OnInitialLoadedAsync() { if (string.IsNullOrWhiteSpace(Program.Args.LaunchPackageName)) { await base.OnInitialLoadedAsync(); return; } await LoadPackages(); var package = Packages.FirstOrDefault(x => x.DisplayName == Program.Args.LaunchPackageName); if (package is not null) { await runningPackageService.StartPackage(package); return; } package = Packages.FirstOrDefault(x => x.Id.ToString() == Program.Args.LaunchPackageName); if (package is null) { await base.OnInitialLoadedAsync(); return; } await runningPackageService.StartPackage(package); } public override async Task OnLoadedAsync() { if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet) return; await LoadPackages(); timer.Start(); } public override void OnUnloaded() { timer.Stop(); base.OnUnloaded(); } public void ShowInstallDialog(BasePackage? selectedPackage = null) { NavigateToSubPage(typeof(PackageInstallBrowserViewModel)); } private async Task LoadPackages() { installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer); var currentUnknown = await Task.Run(IndexUnknownPackages); unknownInstalledPackages.Edit(s => s.Load(currentUnknown)); } private async Task CheckPackagesForUpdates() { foreach (var package in PackageCards) { try { await package.OnLoadedAsync(); } catch (Exception e) { logger.LogError( e, "Failed to check for updates for {Package}", package?.Package?.PackageName ); } } } private IEnumerable IndexUnknownPackages() { var packageDir = settingsManager.LibraryDir.JoinDir("Packages"); if (!packageDir.Exists) { yield break; } var currentPackages = settingsManager.Settings.InstalledPackages.ToImmutableArray(); foreach (var subDir in packageDir.Info.EnumerateDirectories().Select(info => new DirectoryPath(info))) { var expectedLibraryPath = $"Packages{Path.DirectorySeparatorChar}{subDir.Name}"; // Skip if the package is already installed if (currentPackages.Any(p => p.LibraryPath == expectedLibraryPath)) { continue; } if (settingsManager.PackageInstallsInProgress.Contains(subDir.Name)) { continue; } yield return UnknownInstalledPackage.FromDirectoryName(subDir.Name); } } [RelayCommand] private void NavigateToSubPage(Type viewModelType) { Dispatcher.UIThread.Post( () => packageNavigationService.NavigateTo( viewModelType, BetterSlideNavigationTransition.PageSlideFromRight ), DispatcherPriority.Send ); } private void OnInstalledPackagesChanged(object? sender, EventArgs e) => OnLoadedAsync().SafeFireAndForget(); }