510 lines
19 KiB
C#
510 lines
19 KiB
C#
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using Windows.UI;
|
|
using Wino.Core.Domain;
|
|
using Wino.Core.Domain.Entities.Shared;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.Models.Synchronization;
|
|
using Wino.Extensions;
|
|
using Wino.Mail.WinUI.Activation;
|
|
using Wino.Mail.WinUI.Extensions;
|
|
using Wino.Mail.WinUI.Interfaces;
|
|
using Wino.Mail.WinUI.Models;
|
|
using Wino.Mail.WinUI.Views;
|
|
using Wino.Messaging.Client.Shell;
|
|
using Wino.Messaging.UI;
|
|
using WinUIEx;
|
|
|
|
namespace Wino.Mail.WinUI;
|
|
|
|
public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
|
|
IRecipient<ApplicationThemeChanged>,
|
|
IRecipient<InfoBarMessageRequested>,
|
|
IRecipient<TitleBarShellContentUpdated>,
|
|
IRecipient<SynchronizationActionsAdded>,
|
|
IRecipient<SynchronizationActionsCompleted>,
|
|
IRecipient<WinoAccountProfileUpdatedMessage>,
|
|
IRecipient<WinoAccountProfileDeletedMessage>
|
|
{
|
|
public IStatePersistanceService StatePersistanceService { get; } = WinoApplication.Current.Services.GetService<IStatePersistanceService>() ?? throw new Exception("StatePersistanceService not registered in DI container.");
|
|
public IPreferencesService PreferencesService { get; } = WinoApplication.Current.Services.GetService<IPreferencesService>() ?? throw new Exception("PreferencesService not registered in DI container.");
|
|
public INavigationService NavigationService { get; } = WinoApplication.Current.Services.GetService<INavigationService>() ?? throw new Exception("NavigationService not registered in DI container.");
|
|
private IMailDialogService MailDialogService { get; } = WinoApplication.Current.Services.GetRequiredService<IMailDialogService>();
|
|
private IWinoAccountProfileService WinoAccountProfileService { get; } = WinoApplication.Current.Services.GetRequiredService<IWinoAccountProfileService>();
|
|
|
|
public ObservableCollection<SynchronizationActionItem> SyncActionItems { get; } = new();
|
|
private bool _calendarReminderServerStartAttempted;
|
|
private ITitleBarSearchHost? _activeTitleBarSearchHost;
|
|
private bool _isBackButtonVisibilityReady;
|
|
private bool _isSynchronizingTitleBarSearch;
|
|
|
|
public ShellWindow()
|
|
{
|
|
RegisterRecipients();
|
|
|
|
InitializeComponent();
|
|
StatePersistanceService.StatePropertyChanged += StatePersistenceServiceChanged;
|
|
|
|
MinWidth = 420;
|
|
MinHeight = 420;
|
|
ConfigureTitleBar();
|
|
ApplyTitleBarSearchHost();
|
|
|
|
// Handle window closing event to minimize to tray instead of closing
|
|
Closed += OnWindowClosed;
|
|
|
|
// Use the AppWindow.Closing event to handle the close request
|
|
AppWindow.Closing += OnAppWindowClosing;
|
|
|
|
// Register global mouse button listener for back button
|
|
RegisterMouseBackButtonListener();
|
|
|
|
this.SetIcon("Assets/Wino_Icon.ico");
|
|
Title = StatePersistanceService.AppModeTitle;
|
|
}
|
|
|
|
private void ConfigureTitleBar()
|
|
{
|
|
AppWindow.TitleBar.ExtendsContentIntoTitleBar = true;
|
|
|
|
// Apply initial theme colors
|
|
var themeService = WinoApplication.Current.Services.GetService<INewThemeService>();
|
|
if (themeService != null)
|
|
{
|
|
var underlyingThemeService = WinoApplication.Current.Services.GetService<IUnderlyingThemeService>();
|
|
if (underlyingThemeService != null)
|
|
{
|
|
UpdateTitleBarColors(underlyingThemeService.IsUnderlyingThemeDark());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RegisterMouseBackButtonListener()
|
|
{
|
|
// Subscribe to pointer pressed events on the root content
|
|
if (Content is UIElement rootElement)
|
|
{
|
|
rootElement.AddHandler(UIElement.PointerPressedEvent, new Microsoft.UI.Xaml.Input.PointerEventHandler(OnPointerPressed), true);
|
|
}
|
|
}
|
|
|
|
private void OnPointerPressed(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
|
|
{
|
|
// Check if it's the back button (XButton1)
|
|
var pointerPoint = e.GetCurrentPoint(null);
|
|
var properties = pointerPoint.Properties;
|
|
|
|
// XButton1 is the back button on most mice
|
|
if (properties.IsXButton1Pressed)
|
|
{
|
|
// Call GoBack on NavigationService
|
|
NavigationService.GoBack();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
public void HandleAppActivation(string? launchArguments, string? tileId = null, string? appId = null)
|
|
{
|
|
var targetMode = AppModeActivationResolver.Resolve(launchArguments, tileId, appId, PreferencesService.DefaultApplicationMode);
|
|
NavigationService.ChangeApplicationMode(targetMode);
|
|
}
|
|
|
|
public Microsoft.UI.Xaml.Controls.TitleBar GetTitleBar() => ShellTitleBar;
|
|
|
|
public Frame GetMainFrame() => MainShellFrame;
|
|
|
|
public FrameworkElement GetRootContent() => Content as Grid ?? throw new Exception("RootContent is not a Grid or empty.");
|
|
|
|
private void BackButtonClicked(Microsoft.UI.Xaml.Controls.TitleBar sender, object args)
|
|
{
|
|
NavigationService.GoBack();
|
|
}
|
|
|
|
private void MainFrameNavigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
|
{
|
|
if (!_calendarReminderServerStartAttempted)
|
|
{
|
|
_calendarReminderServerStartAttempted = true;
|
|
_ = StartCalendarReminderServerAsync();
|
|
}
|
|
|
|
_isBackButtonVisibilityReady = true;
|
|
ApplyTitleBarSearchHost();
|
|
RefreshBackButtonVisibility();
|
|
}
|
|
|
|
private async Task StartCalendarReminderServerAsync()
|
|
{
|
|
try
|
|
{
|
|
var reminderServer = WinoApplication.Current.Services.GetService<ICalendarReminderServer>();
|
|
if (reminderServer != null)
|
|
{
|
|
await reminderServer.StartAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_calendarReminderServerStartAttempted = false;
|
|
Serilog.Log.Error(ex, "Failed to start calendar reminder server.");
|
|
}
|
|
}
|
|
|
|
private void PaneButtonClicked(Microsoft.UI.Xaml.Controls.TitleBar sender, object args)
|
|
{
|
|
PreferencesService.IsNavigationPaneOpened = !PreferencesService.IsNavigationPaneOpened;
|
|
}
|
|
|
|
public void Receive(TitleBarShellContentUpdated message)
|
|
{
|
|
ApplyTitleBarSearchHost();
|
|
RefreshBackButtonVisibility();
|
|
}
|
|
|
|
public void Receive(ApplicationThemeChanged message)
|
|
{
|
|
UpdateTitleBarColors(message.IsUnderlyingThemeDark);
|
|
}
|
|
|
|
public void Receive(InfoBarMessageRequested message)
|
|
{
|
|
ShowInfoBarMessage(message);
|
|
}
|
|
|
|
public void Receive(SynchronizationActionsAdded message)
|
|
{
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
foreach (var action in message.Actions)
|
|
SyncActionItems.Add(action);
|
|
|
|
UpdateSyncStatusVisibility();
|
|
});
|
|
}
|
|
|
|
public void Receive(SynchronizationActionsCompleted message)
|
|
{
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
var toRemove = SyncActionItems.Where(a => a.AccountId == message.AccountId).ToList();
|
|
|
|
foreach (var item in toRemove)
|
|
SyncActionItems.Remove(item);
|
|
|
|
UpdateSyncStatusVisibility();
|
|
});
|
|
}
|
|
|
|
public void Receive(WinoAccountProfileUpdatedMessage message)
|
|
{
|
|
DispatcherQueue.TryEnqueue(() => UpdateWinoAccountState(message.Account));
|
|
}
|
|
|
|
public void Receive(WinoAccountProfileDeletedMessage message)
|
|
{
|
|
DispatcherQueue.TryEnqueue(() => UpdateWinoAccountState(null));
|
|
}
|
|
|
|
private void UpdateSyncStatusVisibility()
|
|
{
|
|
SyncStatusButton.Visibility = SyncActionItems.Any()
|
|
? Visibility.Visible
|
|
: Visibility.Collapsed;
|
|
|
|
var distinctAccounts = SyncActionItems.Select(a => a.AccountId).Distinct().Count();
|
|
|
|
SyncStatusText.Text = distinctAccounts switch
|
|
{
|
|
0 => string.Empty,
|
|
1 => string.Format(Translator.SyncAction_SynchronizingAccount, SyncActionItems.First().AccountName),
|
|
_ => string.Format(Translator.SyncAction_SynchronizingAccounts, distinctAccounts)
|
|
};
|
|
}
|
|
|
|
private void UpdateTitleBarColors(bool isDarkTheme)
|
|
{
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
var titleBar = AppWindow.TitleBar;
|
|
if (titleBar == null) return;
|
|
|
|
// Set button colors based on theme
|
|
// Background is always transparent for all buttons
|
|
titleBar.ButtonBackgroundColor = Color.FromArgb(0, 0, 0, 0); // Transparent
|
|
titleBar.ButtonInactiveBackgroundColor = Color.FromArgb(0, 0, 0, 0); // Transparent
|
|
titleBar.ButtonHoverBackgroundColor = Color.FromArgb(0, 0, 0, 0); // Transparent
|
|
titleBar.ButtonPressedBackgroundColor = Color.FromArgb(0, 0, 0, 0); // Transparent
|
|
|
|
if (isDarkTheme)
|
|
{
|
|
// Dark theme: use light text/icons for better contrast
|
|
titleBar.ButtonForegroundColor = Color.FromArgb(255, 255, 255, 255); // White
|
|
titleBar.ButtonInactiveForegroundColor = Color.FromArgb(128, 255, 255, 255); // Semi-transparent white
|
|
titleBar.ButtonHoverForegroundColor = Color.FromArgb(255, 255, 255, 255); // White
|
|
titleBar.ButtonPressedForegroundColor = Color.FromArgb(200, 255, 255, 255); // Slightly dimmed white
|
|
}
|
|
else
|
|
{
|
|
// Light theme: use dark text/icons for better contrast
|
|
titleBar.ButtonForegroundColor = Color.FromArgb(255, 0, 0, 0); // Black
|
|
titleBar.ButtonInactiveForegroundColor = Color.FromArgb(128, 0, 0, 0); // Semi-transparent black
|
|
titleBar.ButtonHoverForegroundColor = Color.FromArgb(255, 0, 0, 0); // Black
|
|
titleBar.ButtonPressedForegroundColor = Color.FromArgb(200, 0, 0, 0); // Slightly dimmed black
|
|
}
|
|
});
|
|
}
|
|
|
|
private void ApplyTitleBarSearchHost()
|
|
{
|
|
_activeTitleBarSearchHost = ResolveActiveTitleBarSearchHost();
|
|
SynchronizeTitleBarSearchBox();
|
|
}
|
|
|
|
private void StatePersistenceServiceChanged(object? sender, string propertyName)
|
|
{
|
|
if (!DispatcherQueue.HasThreadAccess)
|
|
{
|
|
var enqueued = DispatcherQueue.TryEnqueue(() => StatePersistenceServiceChanged(sender, propertyName));
|
|
if (!enqueued)
|
|
throw new InvalidOperationException("Could not marshal shell state changes onto the UI thread.");
|
|
|
|
return;
|
|
}
|
|
|
|
if (propertyName == nameof(IStatePersistanceService.ApplicationMode) ||
|
|
propertyName == nameof(IStatePersistanceService.IsReadingMail) ||
|
|
propertyName == nameof(IStatePersistanceService.IsReaderNarrowed) ||
|
|
propertyName == nameof(IStatePersistanceService.IsEventDetailsVisible))
|
|
{
|
|
RefreshBackButtonVisibility();
|
|
}
|
|
}
|
|
|
|
private void RefreshBackButtonVisibility()
|
|
{
|
|
if (!_isBackButtonVisibilityReady)
|
|
{
|
|
ShellTitleBar.IsBackButtonVisible = false;
|
|
return;
|
|
}
|
|
|
|
ShellTitleBar.IsBackButtonVisible = NavigationService.CanGoBack();
|
|
}
|
|
|
|
private ITitleBarSearchHost? ResolveActiveTitleBarSearchHost()
|
|
{
|
|
if (MainShellFrame.Content is WinoAppShell shellPage)
|
|
{
|
|
return shellPage.GetShellFrame().Content as ITitleBarSearchHost;
|
|
}
|
|
|
|
return MainShellFrame.Content as ITitleBarSearchHost;
|
|
}
|
|
|
|
private void SynchronizeTitleBarSearchBox()
|
|
{
|
|
_isSynchronizingTitleBarSearch = true;
|
|
try
|
|
{
|
|
TitleBarSearchBox.IsEnabled = _activeTitleBarSearchHost != null;
|
|
TitleBarSearchBox.PlaceholderText = _activeTitleBarSearchHost?.SearchPlaceholderText ?? Translator.SearchBarPlaceholder;
|
|
TitleBarSearchBox.ItemsSource = _activeTitleBarSearchHost?.SearchSuggestions;
|
|
TitleBarSearchBox.Text = _activeTitleBarSearchHost?.SearchText ?? string.Empty;
|
|
}
|
|
finally
|
|
{
|
|
_isSynchronizingTitleBarSearch = false;
|
|
}
|
|
}
|
|
|
|
private async void TitleBarSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
|
{
|
|
if (_isSynchronizingTitleBarSearch || _activeTitleBarSearchHost == null)
|
|
return;
|
|
|
|
_activeTitleBarSearchHost.SearchText = sender.Text;
|
|
await _activeTitleBarSearchHost.OnTitleBarSearchTextChangedAsync();
|
|
}
|
|
|
|
private void TitleBarSearchSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
|
|
{
|
|
if (_activeTitleBarSearchHost == null || args.SelectedItem is not TitleBarSearchSuggestion suggestion)
|
|
return;
|
|
|
|
_activeTitleBarSearchHost.OnTitleBarSearchSuggestionChosen(suggestion);
|
|
SynchronizeTitleBarSearchBox();
|
|
}
|
|
|
|
private async void TitleBarSearchQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
|
{
|
|
if (_activeTitleBarSearchHost == null)
|
|
return;
|
|
|
|
await _activeTitleBarSearchHost.OnTitleBarSearchSubmittedAsync(args.QueryText, args.ChosenSuggestion as TitleBarSearchSuggestion);
|
|
SynchronizeTitleBarSearchBox();
|
|
}
|
|
|
|
private void OnAppWindowClosing(object sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs e)
|
|
{
|
|
if ((Application.Current as App)?.IsExiting == true)
|
|
return;
|
|
|
|
e.Cancel = true;
|
|
var windowManager = WinoApplication.Current.Services.GetService<IWinoWindowManager>();
|
|
windowManager?.HideWindow(this);
|
|
}
|
|
|
|
private void OnWindowClosed(object sender, WindowEventArgs e)
|
|
{
|
|
AppWindow.Closing -= OnAppWindowClosing;
|
|
StatePersistanceService.StatePropertyChanged -= StatePersistenceServiceChanged;
|
|
UnregisterRecipients();
|
|
}
|
|
|
|
private void RegisterRecipients()
|
|
{
|
|
WeakReferenceMessenger.Default.Register<TitleBarShellContentUpdated>(this);
|
|
WeakReferenceMessenger.Default.Register<ApplicationThemeChanged>(this);
|
|
WeakReferenceMessenger.Default.Register<InfoBarMessageRequested>(this);
|
|
WeakReferenceMessenger.Default.Register<SynchronizationActionsAdded>(this);
|
|
WeakReferenceMessenger.Default.Register<SynchronizationActionsCompleted>(this);
|
|
WeakReferenceMessenger.Default.Register<WinoAccountProfileUpdatedMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<WinoAccountProfileDeletedMessage>(this);
|
|
}
|
|
|
|
private void UnregisterRecipients()
|
|
{
|
|
WeakReferenceMessenger.Default.Unregister<TitleBarShellContentUpdated>(this);
|
|
WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(this);
|
|
WeakReferenceMessenger.Default.Unregister<InfoBarMessageRequested>(this);
|
|
WeakReferenceMessenger.Default.Unregister<SynchronizationActionsAdded>(this);
|
|
WeakReferenceMessenger.Default.Unregister<SynchronizationActionsCompleted>(this);
|
|
WeakReferenceMessenger.Default.Unregister<WinoAccountProfileUpdatedMessage>(this);
|
|
WeakReferenceMessenger.Default.Unregister<WinoAccountProfileDeletedMessage>(this);
|
|
}
|
|
|
|
private void ShowInfoBarMessage(InfoBarMessageRequested message)
|
|
{
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
if (string.IsNullOrEmpty(message.ActionButtonTitle) || message.Action == null)
|
|
{
|
|
ShellInfoBar.ActionButton = null;
|
|
}
|
|
else
|
|
{
|
|
ShellInfoBar.ActionButton = new Button()
|
|
{
|
|
Content = message.ActionButtonTitle,
|
|
Command = new RelayCommand(message.Action)
|
|
};
|
|
}
|
|
|
|
ShellInfoBar.Message = message.Message;
|
|
ShellInfoBar.Title = message.Title;
|
|
ShellInfoBar.Severity = message.Severity.AsMUXCInfoBarSeverity();
|
|
ShellInfoBar.IsOpen = true;
|
|
});
|
|
}
|
|
|
|
private void UpdateWinoAccountState(WinoAccount? account)
|
|
{
|
|
var isSignedIn = account != null;
|
|
|
|
WinoAccountSignedOutView.Visibility = isSignedIn ? Visibility.Collapsed : Visibility.Visible;
|
|
WinoAccountSignedInView.Visibility = isSignedIn ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
WinoAccountButtonPicture.Visibility = isSignedIn ? Visibility.Visible : Visibility.Collapsed;
|
|
WinoAccountSignedOutIcon.Visibility = isSignedIn ? Visibility.Collapsed : Visibility.Visible;
|
|
|
|
var initials = GetInitials(account?.Email);
|
|
|
|
WinoAccountButtonPicture.Initials = initials;
|
|
WinoAccountFlyoutPicture.Initials = initials;
|
|
WinoAccountButtonPicture.DisplayName = account?.Email ?? Translator.WinoAccount_Titlebar_SignedOutTitle;
|
|
WinoAccountFlyoutPicture.DisplayName = account?.Email ?? Translator.WinoAccount_Titlebar_SignedOutTitle;
|
|
|
|
WinoAccountFlyoutEmailText.Text = account?.Email ?? string.Empty;
|
|
WinoAccountFlyoutStatusText.Text = account == null
|
|
? string.Empty
|
|
: string.Format(Translator.WinoAccount_Titlebar_SignedInStatus, account.AccountStatus);
|
|
}
|
|
|
|
private static string GetInitials(string? email)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(email))
|
|
{
|
|
return "W";
|
|
}
|
|
|
|
var localPart = email.Split('@')[0];
|
|
var segments = localPart
|
|
.Split(['.', '_', '-', ' '], StringSplitOptions.RemoveEmptyEntries)
|
|
.Where(segment => !string.IsNullOrWhiteSpace(segment))
|
|
.Take(2)
|
|
.ToArray();
|
|
|
|
if (segments.Length == 0)
|
|
{
|
|
return email[..1].ToUpperInvariant();
|
|
}
|
|
|
|
return string.Concat(segments.Select(segment => char.ToUpperInvariant(segment[0])));
|
|
}
|
|
|
|
private async void RegisterWinoAccountClicked(object sender, RoutedEventArgs e)
|
|
{
|
|
WinoAccountFlyout.Hide();
|
|
var account = await MailDialogService.ShowWinoAccountRegistrationDialogAsync();
|
|
if (account != null)
|
|
{
|
|
ShowInfoBarMessage(new InfoBarMessageRequested(
|
|
InfoBarMessageType.Success,
|
|
Translator.GeneralTitle_Info,
|
|
string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email)));
|
|
}
|
|
}
|
|
|
|
private async void LoginWinoAccountClicked(object sender, RoutedEventArgs e)
|
|
{
|
|
WinoAccountFlyout.Hide();
|
|
var account = await MailDialogService.ShowWinoAccountLoginDialogAsync();
|
|
if (account != null)
|
|
{
|
|
ShowInfoBarMessage(new InfoBarMessageRequested(
|
|
InfoBarMessageType.Success,
|
|
Translator.GeneralTitle_Info,
|
|
string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email)));
|
|
}
|
|
}
|
|
|
|
private async void SignOutWinoAccountClicked(object sender, RoutedEventArgs e)
|
|
{
|
|
var activeAccount = await WinoAccountProfileService.GetActiveAccountAsync();
|
|
if (activeAccount == null)
|
|
{
|
|
ShowInfoBarMessage(new InfoBarMessageRequested(
|
|
InfoBarMessageType.Warning,
|
|
Translator.GeneralTitle_Info,
|
|
Translator.WinoAccount_SignOut_NoAccountMessage));
|
|
return;
|
|
}
|
|
|
|
await WinoAccountProfileService.SignOutAsync();
|
|
|
|
ShowInfoBarMessage(new InfoBarMessageRequested(
|
|
InfoBarMessageType.Success,
|
|
Translator.GeneralTitle_Info,
|
|
string.Format(Translator.WinoAccount_SignOut_SuccessMessage, activeAccount.Email)));
|
|
}
|
|
|
|
}
|