using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.Input; using OwlCore; using OwlCore.AbstractUI.ViewModels; using OwlCore.Extensions; using StrixMusic.Controls; using StrixMusic.Sdk.AppModels; using StrixMusic.Sdk.CoreModels; using StrixMusic.Sdk.ViewModels; using StrixMusic.Services; using StrixMusic.Services.CoreManagement; using StrixMusic.Shared.ViewModels; using Windows.ApplicationModel; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; namespace StrixMusic.Shared { /// /// The SuperShell is a top-level overlay that will always show on top of all other shells. It provides various essential app functions, such as changing settings, setting your shell, viewing debug info, and managing cores. /// public sealed partial class SuperShell : UserControl { private readonly AppSettings _appSettings; private readonly ICoreManagementService _coreManagementService; private readonly SynchronizationContext _syncContext; private readonly LoadedServicesItemViewModel _addNewItem; private AdvancedAppSettingsPanel? _advancedSettings; /// /// Initializes a new instance of the class. /// public SuperShell(AppSettings appSettings, ICoreManagementService coreManagementService) { _appSettings = appSettings; _coreManagementService = coreManagementService; _syncContext = SynchronizationContext.Current ?? new SynchronizationContext(); CancelAddNewCommand = new RelayCommand(() => IsShowingAddNew = false); CancelConfigCoreCommand = new RelayCommand(() => CurrentCoreConfig = null); InitializeComponent(); var registry = CoreRegistry.MetadataRegistry; Guard.IsTrue(registry.Count > 0); foreach (var metadata in registry) AvailableServices.Add(new AvailableServicesItemViewModel(coreManagementService, metadata)); ShellSelectorViewModel = new ShellSelectorViewModel(appSettings); _ = ShellSelectorViewModel.InitAsync(); _addNewItem = new LoadedServicesItemViewModel(true, null); Services.Add(_addNewItem); // The instance is kept in memory and reused in xaml. // Loaded and Unloaded are called as needed when added to and removed from the visual tree, // but the constructor is only called once. Loaded += SuperShell_Loaded; } private void SuperShell_Loaded(object sender, RoutedEventArgs e) { AttachEvents(); _advancedSettings = new AdvancedAppSettingsPanel(_appSettings); AdvancedSettings = new AbstractUICollectionViewModel(_advancedSettings); } private void AttachEvents() { Unloaded += OnUnloaded; var appFrame = Window.Current.GetAppFrame(); Guard.IsNotNull(appFrame.ContentOverlay, nameof(appFrame.ContentOverlay)); appFrame.ContentOverlay.Closed += ContentOverlay_Closed; _addNewItem.NewItemRequested += AddNewItem_NewItemRequested; } private void DetachEvents() { Unloaded -= OnUnloaded; var appFrame = Window.Current.GetAppFrame(); Guard.IsNotNull(appFrame.ContentOverlay, nameof(appFrame.ContentOverlay)); appFrame.ContentOverlay.Closed -= ContentOverlay_Closed; _addNewItem.NewItemRequested -= AddNewItem_NewItemRequested; } private void OnUnloaded(object sender, RoutedEventArgs e) { DetachEvents(); _advancedSettings?.Dispose(); } /// /// The backing dependency property for . /// public static readonly DependencyProperty IsShowingAddNewProperty = DependencyProperty.Register(nameof(IsShowingAddNew), typeof(bool), typeof(SuperShell), new PropertyMetadata(false)); /// /// The backing dependency property for . /// public static readonly DependencyProperty LoadedCoresProperty = DependencyProperty.Register(nameof(LoadedCores), typeof(IReadOnlyList), typeof(SuperShell), new PropertyMetadata(new ICore[] { }, (d, e) => d.Cast().OnLoadedCoresChanged((IReadOnlyList)e.OldValue, (IReadOnlyList)e.NewValue))); /// /// The backing dependency property for . /// public static readonly DependencyProperty SelectedTabIndexProperty = DependencyProperty.Register(nameof(SelectedTabIndex), typeof(int), typeof(SuperShell), new PropertyMetadata(0)); /// /// The backing dependency property for . /// public static readonly DependencyProperty CurrentCoreConfigProperty = DependencyProperty.Register(nameof(CurrentCoreConfig), typeof(CoreViewModel), typeof(SuperShell), new PropertyMetadata(null)); /// /// The backing dependency property for . /// public static readonly DependencyProperty AdvancedSettingsProperty = DependencyProperty.Register(nameof(AdvancedSettings), typeof(AbstractUICollectionViewModel), typeof(SuperShell), new PropertyMetadata(null)); /// /// The cores have been registered and loaded into the app. /// public IReadOnlyList LoadedCores { get => (IReadOnlyList)GetValue(LoadedCoresProperty); set => SetValue(LoadedCoresProperty, value); } /// /// If true, the user has selected to add a new item and the UI should reflect this. /// public bool IsShowingAddNew { get => (bool)GetValue(IsShowingAddNewProperty); set => SetValue(IsShowingAddNewProperty, value); } /// /// The index of the currently selected tab. /// public int SelectedTabIndex { get => (int)GetValue(SelectedTabIndexProperty); set => SetValue(SelectedTabIndexProperty, value); } /// /// If not null, the view should display the config panel for this core /// public CoreViewModel? CurrentCoreConfig { get => (CoreViewModel?)GetValue(CurrentCoreConfigProperty); set => SetValue(CurrentCoreConfigProperty, value); } /// /// The advanced settings for the app. /// public AbstractUICollectionViewModel? AdvancedSettings { get => (AbstractUICollectionViewModel)GetValue(AdvancedSettingsProperty); set => SetValue(AdvancedSettingsProperty, value); } /// /// When fired, the user has canceled adding a new item. /// public IRelayCommand CancelAddNewCommand { get; } /// /// When fired, the user has canceled configuring a core. /// public IRelayCommand CancelConfigCoreCommand { get; } /// /// The services that are available to be added. /// public ObservableCollection AvailableServices { get; } = new(); /// /// The loaded services displayed in the app. /// public ObservableCollection Services { get; } = new(); /// /// Gets the app version number. /// public string AppVersion => $"{Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}"; /// public ShellSelectorViewModel ShellSelectorViewModel { get; } private void AttachEvents(ICore core) { core.CoreStateChanged += Core_CoreStateChanged; } private void DetachEvents(ICore core) { core.CoreStateChanged -= Core_CoreStateChanged; } private void AddNewItem_NewItemRequested(object? sender, EventArgs e) { IsShowingAddNew = true; } private void OnLoadedCoresChanged(IReadOnlyList oldValue, IReadOnlyList newValue) { var removedItems = oldValue.Where(x => !newValue.Contains(x)).ToArray(); var addedItems = newValue.Where(x => !oldValue.Contains(x)).ToArray(); foreach (var item in addedItems) { AttachEvents(item); var loadedServicesViewModel = new LoadedServicesItemViewModel(false, new CoreViewModel(item)); loadedServicesViewModel.ConfigRequested += LoadedService_ConfigRequested; Services.Insert(0, loadedServicesViewModel); } if (addedItems.Length > 0) IsShowingAddNew = false; foreach (var item in removedItems) { DetachEvents(item); var serviceToRemove = Services.First(x => x.Core?.InstanceId == item.InstanceId); serviceToRemove.ConfigRequested -= LoadedService_ConfigRequested; Services.Remove(serviceToRemove); } foreach (var activeServices in Services) activeServices.CanDeleteCore = Services.Count <= 2; } private async void LoadedService_ConfigRequested(object? sender, EventArgs e) { Guard.IsNotNull(sender, nameof(sender)); var viewModel = (LoadedServicesItemViewModel)sender; Guard.IsNotNull(viewModel.Core, nameof(viewModel.Core)); if (viewModel.Core.CoreState == CoreState.Unloaded) { await viewModel.Core.InitAsync(); return; } CurrentCoreConfig = viewModel.Core; } private void Core_CoreStateChanged(object? sender, CoreState e) { Guard.IsNotNull(sender, nameof(sender)); var core = (ICore)sender; if (e is CoreState.Configured or CoreState.Unloaded) { if (CurrentCoreConfig?.InstanceId != core.InstanceId) return; _syncContext.Post(_ => CurrentCoreConfig = null, null); return; } if (e == CoreState.NeedsConfiguration) _syncContext.Post(_ => CurrentCoreConfig = new CoreViewModel(core), null); } private void ContentOverlay_Closed(object? sender, EventArgs e) { CurrentCoreConfig = null; IsShowingAddNew = false; } } }