using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Diagnostics;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using NLog.Config;
using NLog.Targets;
using OwlCore.AbstractStorage;
using OwlCore.AbstractUI.Models;
using OwlCore.Extensions;
using OwlCore.Services;
using StrixMusic.Controls;
using StrixMusic.Cores.LocalFiles;
using StrixMusic.Cores.OneDrive;
using StrixMusic.Cores.OneDrive.Services;
using StrixMusic.Helpers;
using StrixMusic.Sdk.AdapterModels;
using StrixMusic.Sdk.AppModels;
using StrixMusic.Sdk.CoreModels;
using StrixMusic.Sdk.MediaPlayback;
using StrixMusic.Sdk.PluginModels;
using StrixMusic.Sdk.Plugins.PlaybackHandler;
using StrixMusic.Sdk.Plugins.PopulateEmptyNames;
using StrixMusic.Sdk.Services;
using StrixMusic.Sdk.Services.Navigation;
using StrixMusic.Sdk.ViewModels;
using StrixMusic.Sdk.WinUI.Models;
using StrixMusic.Sdk.WinUI.Services.Localization;
using StrixMusic.Sdk.WinUI.Services.NotificationService;
using StrixMusic.Sdk.WinUI.Services.ShellManagement;
using StrixMusic.Services;
using StrixMusic.Services.CoreManagement;
using StrixMusic.Shells.Default;
using StrixMusic.Shells.Groove;
using StrixMusic.Shells.ZuneDesktop;
using Uno.UI.MSAL;
using Windows.ApplicationModel.Core;
using Windows.ApplicationModel.Resources;
using Windows.Storage;
using Windows.UI;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace StrixMusic.Shared
{
///
/// The loading view used to initialize the app on startup. The user sees a splash screen, a text status indicator, and icons representing each core.
///
public sealed partial class AppLoadingView : UserControl
{
private MergedCollectionConfig _mergedCollectionConfig = new();
private bool _showingQuip;
///
/// The cores displayed in the loading UI.
///
public ObservableCollection Cores { get; set; } = new();
///
/// Initializes a new instance of the class.
///
public AppLoadingView()
{
this.InitializeComponent();
#if NETFX_CORE
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true;
var titleBar = ApplicationView.GetForCurrentView().TitleBar;
titleBar.ButtonBackgroundColor = Colors.Transparent;
titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
#endif
}
private void AppLoadingView_OnLoaded(object sender, RoutedEventArgs e)
{
PrereleaseNoticeContainer.Visibility = Visibility.Collapsed;
_ = InitAsync();
}
private async Task InitAsync()
{
var appFrame = Window.Current.GetAppFrame();
UpdateStatusRaw("> ...");
Logger.LogInformation("Setting up app settings");
var settings = await InitAppSettings();
settings.PropertyChanged += OnSettingChanged;
_mergedCollectionConfig.MergedCollectionSorting = settings.MergedCollectionSorting;
if (settings.IsLoggingEnabled)
{
Logger.LogInformation("Setting up persistent logging");
SetupLogger();
}
Logger.LogInformation("Setting up localization");
var localizationService = appFrame.LocalizationService;
Logger.LogInformation("Enabling quips");
ShowQuip(localizationService); // TODO: Add debug boot mode
Logger.LogInformation("Setting up playback and SMTP handlers");
var playbackHandlerService = new PlaybackHandlerService();
var smtpHandler = new SystemMediaTransportControlsHandler(playbackHandlerService);
Logger.LogInformation("Setting up navigation service");
var navService = new NavigationService();
Logger.LogInformation("Initializing core registry");
UpdateStatus("InitCoreReg", localizationService);
InitializeCoreRegistry(appFrame.NotificationService);
Logger.LogInformation("Initializing shell registry");
await InitializeShellRegistryAsync(appFrame.NotificationService);
Logger.LogInformation($"Initializing {nameof(FileSystemService)}");
var fileSystemService = new FileSystemService();
await fileSystemService.InitAsync();
Logger.LogInformation($"Initializing {nameof(CoreManagementService)}");
var cores = new List();
var coreManagementService = new CoreManagementService(settings);
await coreManagementService.InitAsync();
coreManagementService.CoreInstanceRegistered += OutOfBox_CoreInstanceRegistered;
Logger.LogInformation("Checking for valid core registry");
var registry = await coreManagementService.GetCoreInstanceRegistryAsync();
if (registry.Count == 0)
{
Logger.LogInformation("No registered core instances. Displaying OOBE, waiting for at least 1 core to be configured.");
await WaitForTempOutOfBoxSetupAsync(coreManagementService, appFrame.NotificationService, settings, cores);
}
coreManagementService.CoreInstanceRegistered -= OutOfBox_CoreInstanceRegistered;
Guard.IsTrue(settings.CoreInstanceRegistry.Count > 0, nameof(settings.CoreInstanceRegistry));
Logger.LogInformation($"Initializing core ranking");
_mergedCollectionConfig.CoreRanking = settings.CoreRanking = await InitializeCoreRankingAsync(coreManagementService);
Guard.HasSizeGreaterThan(settings.CoreRanking, 0, nameof(settings.CoreRanking));
Logger.LogInformation("Constructing cores");
UpdateStatus("CreatingCores", localizationService);
var registeredCoreInstances = await coreManagementService.GetCoreInstanceRegistryAsync();
foreach (var entry in registeredCoreInstances)
{
if (cores.FirstOrDefault(x => x.InstanceId == entry.Key) is not ICore core)
{
core = await CoreRegistry.CreateCoreAsync(entry.Value.Id, entry.Key);
cores.Add(core);
}
core.CoreStateChanged += RegisteredCore_OnStateChanged;
Cores.Add(new CoreViewModel(core));
}
Logger.LogInformation("Constructing inbox plugins");
var playbackHandlerPlugin = new PlaybackHandlerPlugin(playbackHandlerService);
var emptyNameFallbackPlugin = new PopulateEmptyNamesPlugin
{
EmptyAlbumName = localizationService.Music?.GetString("UnknownAlbum") ?? "?",
EmptyArtistName = localizationService.Music?.GetString("UnknownArtist") ?? "?",
EmptyDefaultName = localizationService.Music?.GetString("UnknownName") ?? "?",
};
Logger.LogInformation("Constructing data layers");
var mergedLayer = new MergedCore(cores, _mergedCollectionConfig);
var pluginLayer = new StrixDataRootPluginWrapper(mergedLayer, emptyNameFallbackPlugin, playbackHandlerPlugin);
var rootViewModel = new StrixDataRootViewModel(pluginLayer);
Logger.LogInformation("Setting up primary app view");
var mainPage = new MainPage(rootViewModel, settings, coreManagementService);
Logger.LogInformation("Setting up misc lifetime events");
Guard.IsNotNull(appFrame.ContentOverlay);
appFrame.ContentOverlay.Closed += ContentOverlay_Closed;
coreManagementService.CoreInstanceRegistered += Lifetime_CoreInstanceRegistered;
coreManagementService.CoreInstanceUnregistered += OnCoreInstanceUnregistered;
UpdateStatus("InitServices", localizationService);
Logger.LogInformation("Setting up IoC");
{
var services = new ServiceCollection();
services.AddSingleton>(navService);
services.AddSingleton(playbackHandlerService);
services.AddSingleton(smtpHandler);
services.AddSingleton(rootViewModel);
services.AddSingleton(localizationService);
services.AddSingleton(settings);
services.AddSingleton(fileSystemService);
services.AddSingleton(appFrame.NotificationService);
services.AddSingleton(coreManagementService);
Ioc.Default.ConfigureServices(services.BuildServiceProvider());
}
Logger.LogInformation("Initializing data root and cores");
UpdateStatus("InitCores", localizationService);
await rootViewModel.InitAsync();
Logger.LogInformation("Setting up audio containers");
foreach (var core in cores)
{
if (core.PlaybackType == MediaPlayerType.Standard)
{
var audioPlayer = new AudioPlayerService(mainPage.CreateMediaPlayerElement());
playbackHandlerService.RegisterAudioPlayer(audioPlayer, core.InstanceId);
}
}
Logger.LogInformation("Loading completed, saving settings");
await settings.SaveAsync();
UpdateStatusRaw("Done loading");
StrixIcon strixIcon = PART_StrixIcon;
strixIcon.FinishAnimation();
await OwlCore.Flow.EventAsTask(x => strixIcon.AnimationFinished += x, x => strixIcon.AnimationFinished -= x, CancellationToken.None);
foreach (var core in Cores.ToList())
{
Cores.Remove(core);
await Task.Delay(75);
}
appFrame.Present(mainPage);
void RegisteredCore_OnStateChanged(object? sender, CoreState e)
{
var core = sender as ICore;
Guard.IsNotNull(core);
if (e == CoreState.NeedsConfiguration)
appFrame.DisplayAbstractUIPanel(core, settings, cores, coreManagementService);
}
async void ContentOverlay_Closed(object? sender, EventArgs e)
{
Guard.IsNotNull(rootViewModel);
Guard.IsNotNull(coreManagementService);
foreach (var core in rootViewModel.Sources.ToArray())
{
if (core.CoreState == CoreState.Unloaded)
{
await coreManagementService.UnregisterCoreInstanceAsync(core.InstanceId);
}
}
}
async void OutOfBox_CoreInstanceRegistered(object? sender, CoreInstanceEventArgs args)
{
var core = await CoreRegistry.CreateCoreAsync(args.CoreMetadata.Id, args.InstanceId);
cores.Add(core);
core.CoreStateChanged += Core_CoreStateChanged;
await core.InitAsync();
core.CoreStateChanged -= Core_CoreStateChanged;
void Core_CoreStateChanged(object? sender, CoreState e)
{
if (e == CoreState.NeedsConfiguration)
appFrame.DisplayAbstractUIPanel(core, settings, cores, coreManagementService);
}
}
async void Lifetime_CoreInstanceRegistered(object? sender, CoreInstanceEventArgs args)
{
var core = await CoreRegistry.CreateCoreAsync(args.CoreMetadata.Id, args.InstanceId);
core.CoreStateChanged += RegisteredCore_OnStateChanged;
mergedLayer.AddSource(core);
cores.Add(core);
Cores.Add(new CoreViewModel(core));
await core.InitAsync();
if (core.PlaybackType == MediaPlayerType.Standard)
{
var audioPlayer = new AudioPlayerService(mainPage.CreateMediaPlayerElement());
playbackHandlerService.RegisterAudioPlayer(audioPlayer, core.InstanceId);
}
}
void Core_CoreStateChanged(object? sender, CoreState e)
{
var core = sender as ICore;
Guard.IsNotNull(core);
if (e == CoreState.NeedsConfiguration)
appFrame.DisplayAbstractUIPanel(core, settings, rootViewModel.Sources, coreManagementService);
}
void OnCoreInstanceUnregistered(object? sender, CoreInstanceEventArgs e)
{
var sourceToRemove = mergedLayer.Sources.First(x => x.InstanceId == e.InstanceId);
sourceToRemove.CoreStateChanged -= Core_CoreStateChanged;
mergedLayer.RemoveSource(sourceToRemove);
cores.Remove(sourceToRemove);
}
}
private static async Task InitializeShellRegistryAsync(NotificationService notificationService)
{
ShellRegistry.ShellRegistered += OnShellRegistered;
var shellFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("Shells", Windows.Storage.CreationCollisionOption.OpenIfExists);
var zuneSettingsStorage = await shellFolder.CreateFolderAsync(ZuneShell.Metadata.Id, Windows.Storage.CreationCollisionOption.OpenIfExists);
ShellRegistry.Register(DefaultShell.Metadata, dataRoot => new DefaultShell(dataRoot));
ShellRegistry.Register(GrooveShell.Metadata, dataRoot => new GrooveShell(dataRoot));
ShellRegistry.Register(ZuneShell.Metadata, dataRoot => new ZuneShell(dataRoot, new FolderData(zuneSettingsStorage), notificationService));
if (ShellRegistry.MetadataRegistry.Count == 0)
ThrowHelper.ThrowInvalidOperationException($"{nameof(ShellRegistry.MetadataRegistry)} contains no elements after registry initialization. App cannot function without at least 1 shell.");
ShellRegistry.ShellRegistered -= OnShellRegistered;
void OnShellRegistered(object? sender, ShellMetadata metadata) => Logger.LogInformation($"Shell registered. Id {metadata.Id}, Display name {metadata.DisplayName}");
}
private static void InitializeCoreRegistry(NotificationService notificationService)
{
CoreRegistry.CoreRegistered += OnCoreRegistered;
CoreRegistry.Register(LocalFilesCore.Metadata, async instanceId =>
{
var coreFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("Cores", Windows.Storage.CreationCollisionOption.OpenIfExists);
var coreInstanceFolder = await coreFolder.CreateFolderAsync(instanceId, Windows.Storage.CreationCollisionOption.OpenIfExists);
return new LocalFilesCore(instanceId, new FileSystemService(coreInstanceFolder), new FolderData(coreInstanceFolder), notificationService);
});
CoreRegistry.Register(OneDriveCore.Metadata, async instanceId =>
{
var coreFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("Cores", Windows.Storage.CreationCollisionOption.OpenIfExists);
var coreInstanceFolder = await coreFolder.CreateFolderAsync(instanceId, Windows.Storage.CreationCollisionOption.OpenIfExists);
var coreInstanceAbstractFolder = new FolderData(coreInstanceFolder);
var loginMethod = LoginMethod.DeviceCode;
HttpMessageHandler messageHandler = new HttpClientHandler();
var settings = new OneDriveCoreSettings(coreInstanceAbstractFolder);
await settings.LoadAsync();
if (string.IsNullOrWhiteSpace(settings.ClientId))
settings.ClientId = Secrets.OneDriveClientId;
if (string.IsNullOrWhiteSpace(settings.TenantId))
settings.TenantId = Secrets.OneDriveTenantId;
if (string.IsNullOrWhiteSpace(settings.RedirectUri))
settings.RedirectUri = Secrets.OneDriveRedirectUri;
await settings.SaveAsync();
#if __WASM__
loginMethod = LoginMethod.Interactive;
messageHandler = new Uno.UI.Wasm.WasmHttpHandler();
#endif
var core = new OneDriveCore(instanceId, settings, coreInstanceAbstractFolder, notificationService)
{
LoginMethod = loginMethod,
HttpMessageHandler = messageHandler,
};
// TODO: Detach event when unregistered
core.LoginNavigationRequested += OnUriNavigationRequested;
core.MsalAcquireTokenInteractiveParameterBuilderCreated += OnInteractiveParamBuilderCreated;
core.MsalPublicClientApplicationBuilderCreated += OnPublicClientApplicationBuilderCreated;
return core;
void OnInteractiveParamBuilderCreated(object? sender, AcquireTokenInteractiveParameterBuilderCreatedEventArgs args) => args.Builder = args.Builder.WithUnoHelpers();
void OnPublicClientApplicationBuilderCreated(object? sender, MsalPublicClientApplicationBuilderCreatedEventArgs args) => args.Builder = args.Builder.WithUnoHelpers();
});
if (CoreRegistry.MetadataRegistry.Count == 0)
ThrowHelper.ThrowInvalidOperationException($"{nameof(CoreRegistry.MetadataRegistry)} contains no elements after registry initialization. App cannot function without at least 1 core.");
CoreRegistry.CoreRegistered -= OnCoreRegistered;
void OnCoreRegistered(object? sender, CoreMetadata metadata) => Logger.LogInformation($"Core registered. Id {metadata.Id}, Display name {metadata.DisplayName}");
}
private static void OnUriNavigationRequested(object? sender, Uri e) => Windows.System.Launcher.LaunchUriAsync(e);
private static async Task> InitializeCoreRankingAsync(ICoreManagementService coreManager)
{
#warning TODO: Move into core management service.
var coreInstanceRegistry = await coreManager.GetCoreInstanceRegistryAsync();
Guard.IsGreaterThan(coreInstanceRegistry.Count, 0, nameof(coreInstanceRegistry.Count));
var existingCoreRanking = await coreManager.GetCoreInstanceRanking();
var coreRanking = new List();
foreach (var instanceId in existingCoreRanking)
{
var coreMetadata = coreInstanceRegistry.FirstOrDefault(x => x.Key == instanceId).Value;
if (coreMetadata is null)
continue;
// If this core is still registered, add it to the ranking.
if (CoreRegistry.MetadataRegistry.Any(x => x.Id == coreMetadata.Id))
coreRanking.Add(instanceId);
}
// If no cores exist in ranking, initialize with all loaded cores.
if (coreRanking.Count == 0)
{
Logger.LogInformation($"No existing core rankings found, creating default ranking...");
// TODO: Show abstractUI and let the user rank the cores manually.
foreach (var instance in coreInstanceRegistry)
coreRanking.Add(instance.Key);
}
Guard.IsGreaterThan(coreRanking.Count, 0, nameof(coreRanking.Count));
Logger.LogInformation($"Core ranking initialized with {coreRanking.Count} instances.");
return coreRanking;
}
private void OnSettingChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var appSettings = sender as AppSettings;
Guard.IsNotNull(appSettings, nameof(appSettings));
if (e.PropertyName == nameof(AppSettings.CoreRanking))
_mergedCollectionConfig.CoreRanking = appSettings.CoreRanking;
if (e.PropertyName == nameof(AppSettings.MergedCollectionSorting))
_mergedCollectionConfig.MergedCollectionSorting = appSettings.MergedCollectionSorting;
}
private static async Task WaitForTempOutOfBoxSetupAsync(ICoreManagementService coreManagementService, INotificationService notificationService, AppSettings settings, IReadOnlyList loadedCores)
{
var doneButton = new AbstractButton($"{nameof(AppLoadingView)}.OOBEFinishedButton", "Done", null, AbstractButtonType.Confirm);
var tempOOBEContinuationUI = new AbstractUICollection($"{nameof(AppLoadingView)}.OOBEElementGroup", PreferredOrientation.Horizontal)
{
Title = "First time?",
Subtitle = "Set up your skins and services before proceeding. A proper OOBE will come later.",
};
tempOOBEContinuationUI.Add(doneButton);
var notification = notificationService.RaiseNotification(tempOOBEContinuationUI);
// TODO Need a real OOBE instead of using SuperShell
Window.Current.GetAppFrame().DisplaySuperShell(settings, loadedCores, coreManagementService);
// TODO Temp, not great. Need a proper flow here.
var setupFinishedSemaphore = new SemaphoreSlim(0, 1);
notification.Dismissed += OnNotificationDismissed;
doneButton.Clicked += OnDoneButtonClicked;
async void OnNotificationDismissed(object? sender, EventArgs e)
{
Logger.LogInformation($"{nameof(WaitForTempOutOfBoxSetupAsync)}: {nameof(OnNotificationDismissed)}");
var registry = await coreManagementService.GetCoreInstanceRegistryAsync();
if (registry.Count == 0)
await WaitForTempOutOfBoxSetupAsync(coreManagementService, notificationService, settings, loadedCores);
setupFinishedSemaphore.Release();
}
async void OnDoneButtonClicked(object? sender, EventArgs e)
{
Logger.LogInformation($"{nameof(WaitForTempOutOfBoxSetupAsync)}: {nameof(OnDoneButtonClicked)}");
var registry = await coreManagementService.GetCoreInstanceRegistryAsync();
if (registry.Count > 0)
setupFinishedSemaphore.Release();
}
await setupFinishedSemaphore.WaitAsync();
Logger.LogInformation("OOBE completed");
notification.Dismissed -= OnNotificationDismissed;
doneButton.Clicked -= OnDoneButtonClicked;
notification.Dismiss();
}
private void ShowQuip(LocalizationResourceLoader localizationService)
{
var quip = new QuipLoader(CultureInfo.CurrentCulture.TwoLetterISOLanguageName).GetGroupIndexQuip();
PART_Status.Text = localizationService.Quips?.GetString($"{quip.Item1}{quip.Item2}") ?? string.Empty;
_showingQuip = true;
}
private void UpdateStatus(string text, LocalizationResourceLoader localizationService)
{
if (_showingQuip)
return;
if (localizationService != null)
text = localizationService?.Startup?.GetString(text) ?? string.Empty;
PART_Status.Text = text;
}
private void UpdateStatusRaw(string text)
{
PART_Status.Text = text;
}
private static async Task InitAppSettings()
{
var settingsDirectory = await ApplicationData.Current.LocalFolder.CreateFolderAsync(nameof(AppSettings), Windows.Storage.CreationCollisionOption.OpenIfExists);
var settings = new AppSettings(new FolderData(settingsDirectory));
await settings.LoadAsync();
return settings;
}
private static void SetupLogger()
{
var logPath = ApplicationData.Current.LocalCacheFolder.Path + @"\Logs\${date:format=yyyy-MM-dd}.log";
NLog.LogManager.Configuration = CreateConfig(shouldArchive: true);
// Event is connected for the lifetime of the application
Logger.MessageReceived += Logger_MessageReceived;
Logger.LogInformation("Logger initialized");
LoggingConfiguration CreateConfig(bool shouldArchive)
{
var config = new LoggingConfiguration();
var fileTarget = new FileTarget("filelog")
{
FileName = logPath,
EnableArchiveFileCompression = shouldArchive,
MaxArchiveDays = 7,
ArchiveNumbering = ArchiveNumberingMode.Sequence,
ArchiveOldFileOnStartup = shouldArchive,
KeepFileOpen = true,
OpenFileCacheTimeout = 10,
AutoFlush = false,
OpenFileFlushTimeout = 10,
ConcurrentWrites = false,
CleanupFileName = false,
OptimizeBufferReuse = true,
Layout = "${message}",
};
config.AddTarget(fileTarget);
config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, "filelog");
var debuggerTarget = new DebuggerTarget("debuggerTarget")
{
OptimizeBufferReuse = true,
Layout = "${message}",
};
config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, debuggerTarget);
config.AddTarget(debuggerTarget);
return config;
}
}
private static void Logger_MessageReceived(object? sender, LoggerMessageEventArgs e)
{
var message = $"{DateTime.UtcNow:O} [{e.Level}] [Thread {Thread.CurrentThread.ManagedThreadId}] L{e.CallerLineNumber} {System.IO.Path.GetFileName(e.CallerFilePath)} {e.CallerMemberName} {(e.Exception is not null ? $"Exception: {e.Exception} |" : string.Empty)} {e.Message}";
NLog.LogManager.GetLogger(string.Empty).Log(NLog.LogLevel.Info, message);
Console.WriteLine(message);
}
}
}