/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System; using System.Threading; using System.Threading.Tasks; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using ProtonVPN.Client.Common.Dispatching; using ProtonVPN.Client.Common.Models; using ProtonVPN.Client.Common.UI.Controls.Custom; using ProtonVPN.Client.Common.UI.Helpers; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Bases.ViewModels; using ProtonVPN.Client.Core.Extensions; using ProtonVPN.Client.Core.Services.Mapping; using ProtonVPN.Client.Core.Services.Selection; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Common.Core.Threading; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; using ProtonVPN.Client.Localization.Contracts; namespace ProtonVPN.Client.Core.Services.Activation.Bases; public abstract class OverlayActivatorBase : WindowHostActivatorBase, IOverlayActivator where TWindow : BaseWindow { protected readonly IOverlayViewMapper OverlayViewMapper; private readonly SemaphoreSlim _overlaySemaphore = new(1, 1); private readonly AsyncManualResetEventSlim _isLoadedGate = new(); private ContentDialog? _currentOverlay; public bool HasActiveOverlay => _currentOverlay != null; protected OverlayActivatorBase( ILogger logger, IUIThreadDispatcher uiThreadDispatcher, IApplicationThemeSelector themeSelector, ISettings settings, ILocalizationService localizationService, IOverlayViewMapper overlayViewMapper) : base(logger, uiThreadDispatcher, themeSelector, settings, localizationService) { OverlayViewMapper = overlayViewMapper; } public event EventHandler? OverlayChanged; public void CloseCurrentOverlay() { _currentOverlay?.Hide(); } public OverlayViewModelBase? GetCurrentOverlayContext() { return _currentOverlay is IContextAware contextAware ? contextAware.GetContext() as OverlayViewModelBase : null; } public Task ShowMessageAsync(MessageDialogParameters parameters) { MessageContentDialog overlay = CreateMessageOverlay(parameters); return ShowOverlayAsync(overlay); } public async Task ShowLoadingMessageAsync(MessageDialogParameters parameters, Task loadingTask) { MessageContentDialog overlay = CreateMessageOverlay(parameters); Task overlayTask = ShowOverlayAsync(overlay); await Task.WhenAny( overlayTask, loadingTask); if (!overlayTask.IsCompleted) { overlay.Hide(); } return ContentDialogResult.None; } public Task ShowOverlayAsync(OverlayViewModelBase overlayViewModel, object? parameter = null) { Type overlayType = OverlayViewMapper.GetViewType(overlayViewModel.GetType()); return ShowOverlayAsync(overlayType, parameter); } protected Task ShowOverlayAsync(object? parameter = null) where TViewModel : OverlayViewModelBase { Type overlayType = OverlayViewMapper.GetViewType(); return ShowOverlayAsync(overlayType, parameter); } protected override void OnWindowLoaded() { base.OnWindowLoaded(); // Host ready, release the semaphore to allow showing overlays _isLoadedGate.Set(); } protected override void OnReset() { base.OnReset(); // Host has been reset, reset the semaphore to prevent showing overlays _isLoadedGate.Reset(); } protected override void RegisterToHostEvents() { base.RegisterToHostEvents(); if (Host != null) { Host.Closed += OnWindowClosed; } } protected override void UnregisterFromHostEvents() { base.UnregisterFromHostEvents(); if (Host != null) { Host.Closed -= OnWindowClosed; } } protected override void OnFlowDirectionChanged() { base.OnFlowDirectionChanged(); if (_currentOverlay != null) { _currentOverlay.FlowDirection = CurrentFlowDirection; } } protected override void OnAppThemeChanged() { base.OnAppThemeChanged(); if (_currentOverlay != null) { _currentOverlay.RequestedTheme = CurrentAppTheme; } } private void OnWindowClosed(object sender, WindowEventArgs e) { if (!e.Handled) { Reset(); } } private async Task ShowOverlayAsync(Type overlayType, object? parameter) { ContentDialog? overlay = Activator.CreateInstance(overlayType) as ContentDialog; if (overlay == null) { Logger.Error($"Type {overlayType} is not recognized as a ContentDialog."); return ContentDialogResult.None; } return await ShowOverlayAsync(overlay, parameter); } private async Task ShowOverlayAsync(ContentDialog overlay, object? parameter = null) { // Close current overlayViewModel if any, then register the new one. Cannot have two overlays at the same time CloseCurrentOverlay(); await _isLoadedGate.WaitAsync(); await _overlaySemaphore.WaitAsync(); try { RegisterOverlay(overlay); // Activate host window to bring it to foreground before showing the overlayViewModel Host?.Activate(); IOverlayActivationAware? context = GetCurrentOverlayContext(); context?.OnShow(parameter); ContentDialogResult result = await overlay.ShowAsync(); return result; } catch (Exception e) { Logger.Error($"Error when trying to show message '{overlay}'", e); return ContentDialogResult.None; } finally { UnregisterCurrentOverlay(); _overlaySemaphore.Release(); } } private void RegisterOverlay(ContentDialog overlay) { if (Host == null) { throw new InvalidOperationException("Host window has not been initialized."); } overlay.XamlRoot = Host.GetXamlRoot(); overlay.RequestedTheme = CurrentAppTheme; overlay.FlowDirection = CurrentFlowDirection; _currentOverlay = overlay; OverlayChanged?.Invoke(this, EventArgs.Empty); } private void UnregisterCurrentOverlay() { _currentOverlay = null; OverlayChanged?.Invoke(this, EventArgs.Empty); } private MessageContentDialog CreateMessageOverlay(MessageDialogParameters parameters) { MessageContentDialog overlay = new() { Title = parameters.Title, Content = GetMessageContent(parameters), IsLoading = parameters.ShowLoadingAnimation, IsVerticalLayout = parameters.UseVerticalLayoutForButtons, PrimaryButtonText = parameters.PrimaryButtonText, SecondaryButtonText = parameters.SecondaryButtonText, CloseButtonText = parameters.CloseButtonText }; return overlay; } private object GetMessageContent(MessageDialogParameters parameters) { return string.IsNullOrEmpty(parameters.Message) ? string.Empty : parameters.MessageType switch { DialogMessageType.RichText => new RichTextBlock { Blocks = { RichTextHelper.ParseRichText(parameters.Message, parameters.TrailingInlineButton) } }, _ => parameters.Message, }; } }