From 2b523d64e86359a4f58f304830e32b700d9fa4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 11 Mar 2026 01:39:32 +0100 Subject: [PATCH] New shell experience. --- AGENTS.md | 2 + .../CalendarAppShellViewModel.cs | 37 +- Wino.Core.Domain/Interfaces/IShellClient.cs | 73 ++ .../MenuItems/NewCalendarEventMenuItem.cs | 3 + .../Navigation/ShellModeActivationContext.cs | 6 + .../Translations/en_US/resources.json | 1 - Wino.Mail.ViewModels/MailAppShellViewModel.cs | 31 +- Wino.Mail.WinUI/App.xaml | 98 ++- Wino.Mail.WinUI/App.xaml.cs | 8 + .../AppModeFooterSwitcherControl.xaml | 4 +- .../AppModeFooterSwitcherControl.xaml.cs | 4 - Wino.Mail.WinUI/CoreGeneric.xaml | 14 +- .../Properties/launchSettings.json | 3 +- .../NavigationMenuTemplateSelector.cs | 5 +- Wino.Mail.WinUI/Services/NavigationService.cs | 72 +- Wino.Mail.WinUI/ShellWindow.xaml | 2 +- Wino.Mail.WinUI/ShellWindow.xaml.cs | 13 +- .../ViewModels/ContactsShellClient.cs | 39 + .../ViewModels/WinoAppShellViewModel.cs | 131 ++++ .../Views/Abstract/WinoAppShellAbstract.cs | 11 + .../Views/Calendar/CalendarAppShell.xaml | 3 +- Wino.Mail.WinUI/Views/WinoAppShell.xaml | 730 ++++++++++++++++++ Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs | 679 ++++++++++++++++ 23 files changed, 1901 insertions(+), 68 deletions(-) create mode 100644 Wino.Core.Domain/Interfaces/IShellClient.cs create mode 100644 Wino.Core.Domain/MenuItems/NewCalendarEventMenuItem.cs create mode 100644 Wino.Core.Domain/Models/Navigation/ShellModeActivationContext.cs create mode 100644 Wino.Mail.WinUI/ViewModels/ContactsShellClient.cs create mode 100644 Wino.Mail.WinUI/ViewModels/WinoAppShellViewModel.cs create mode 100644 Wino.Mail.WinUI/Views/Abstract/WinoAppShellAbstract.cs create mode 100644 Wino.Mail.WinUI/Views/WinoAppShell.xaml create mode 100644 Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs diff --git a/AGENTS.md b/AGENTS.md index 0d33bd8b..f1324382 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -142,6 +142,8 @@ private string searchQuery = string.Empty; - For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations. - In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`). - In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`. +- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML. +- For XAML-backed controls and pages, wire framework events like `Loaded`, `Unloaded`, and input events from XAML, not by subscribing in constructors in `.xaml.cs`. diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs index 42789c62..8a661682 100644 --- a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs @@ -30,6 +30,7 @@ using Wino.Messaging.UI; namespace Wino.Calendar.ViewModels; public partial class CalendarAppShellViewModel : CalendarBaseViewModel, + ICalendarShellClient, IRecipient, IRecipient, IRecipient, @@ -40,6 +41,17 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, public IStatePersistanceService StatePersistenceService { get; } public IAccountCalendarStateService AccountCalendarStateService { get; } public INavigationService NavigationService { get; } + public WinoApplicationMode Mode => WinoApplicationMode.Calendar; + public bool HandlesNavigationSelection => false; + System.Collections.IEnumerable ICalendarShellClient.GroupedAccountCalendars => AccountCalendarStateService.GroupedAccountCalendars; + System.Collections.IEnumerable ICalendarShellClient.DateNavigationHeaderItems => DateNavigationHeaderItems; + object IShellClient.SelectedMenuItem + { + get => null; + set { } + } + System.Windows.Input.ICommand ICalendarShellClient.TodayClickedCommand => TodayClickedCommand; + System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand; public MenuItemCollection MenuItems { get; private set; } public MenuItemCollection FooterItems { get; private set; } @@ -78,7 +90,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, private readonly ManageAccountsMenuItem _manageAccountsMenuItem = new(); private readonly SettingsItem _settingsItem = new(); private readonly StoreUpdateMenuItem _storeUpdateMenuItem = new(); - private readonly NewMailMenuItem _newEventMenuItem = new(); + private readonly NewCalendarEventMenuItem _newEventMenuItem = new(); // For updating account calendars asynchronously. private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1); @@ -146,6 +158,10 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, { base.OnNavigatedTo(mode, parameters); + var activationContext = parameters as ShellModeActivationContext; + var isModeResetActivation = activationContext != null; + var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true; + PreferencesService.PreferenceChanged -= PreferencesServiceChanged; PreferencesService.PreferenceChanged += PreferencesServiceChanged; @@ -155,7 +171,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, // between Mail and Calendar modes. Back/forward restoration should not // force a new CalendarPage navigation, otherwise pages like // CalendarEventComposePage get dropped from the inner frame stack. - if (mode != NavigationMode.New) + if (mode != NavigationMode.New && !isModeResetActivation) { UpdateDateNavigationHeaderItems(); await InitializeAccountCalendarsAsync(); @@ -168,7 +184,10 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, await InitializeAccountCalendarsAsync(); ValidateConfiguredNewEventCalendar(); - await ShowWhatIsNewIfNeededAsync(); + if (shouldRunStartupFlows) + { + await ShowWhatIsNewIfNeededAsync(); + } TodayClicked(); } @@ -601,6 +620,18 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, return (startDate, startDate.AddMinutes(30)); } + + void IShellClient.Activate(ShellModeActivationContext activationContext) + => OnNavigatedTo(NavigationMode.New, activationContext); + + void IShellClient.Deactivate() + => OnNavigatedFrom(NavigationMode.New, null!); + + Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem) + => menuItem == null ? Task.CompletedTask : HandleNavigationItemInvokedAsync(menuItem); + + Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem) + => Task.CompletedTask; } diff --git a/Wino.Core.Domain/Interfaces/IShellClient.cs b/Wino.Core.Domain/Interfaces/IShellClient.cs new file mode 100644 index 00000000..321d5442 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IShellClient.cs @@ -0,0 +1,73 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.MenuItems; +using Wino.Core.Domain.Models; +using Wino.Core.Domain.Models.Calendar; +using Wino.Core.Domain.Models.Folders; +using Wino.Core.Domain.Models.Navigation; + +namespace Wino.Core.Domain.Interfaces; + +public interface IShellClient : INotifyPropertyChanged +{ + WinoApplicationMode Mode { get; } + IDispatcher Dispatcher { get; set; } + MenuItemCollection? MenuItems { get; } + object? SelectedMenuItem { get; set; } + bool HandlesNavigationSelection { get; } + + void Activate(ShellModeActivationContext activationContext); + void Deactivate(); + Task HandleNavigationItemInvokedAsync(IMenuItem? menuItem); + Task HandleNavigationSelectionChangedAsync(IMenuItem? menuItem); + Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args); +} + +public interface IMailShellClient : IShellClient +{ + IMenuItem CreatePrimaryMenuItem { get; } + + IEnumerable GetFolderContextMenuActions(IBaseFolderMenuItem folder); + Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem, TaskCompletionSource? folderInitAwaitTask = null); + Task ChangeLoadedAccountAsync(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true); + Task PerformFolderOperationAsync(FolderOperation operation, IBaseFolderMenuItem folderMenuItem); + Task PerformMoveOperationAsync(IEnumerable items, IBaseFolderMenuItem targetFolderMenuItem); + Task CreateNewMailForAsync(MailAccount account); +} + +public interface ICalendarShellClient : IShellClient +{ + IStatePersistanceService StatePersistenceService { get; } + IEnumerable DateNavigationHeaderItems { get; } + int SelectedDateNavigationHeaderIndex { get; } + DateRange? HighlightedDateRange { get; } + ICommand TodayClickedCommand { get; } + ICommand DateClickedCommand { get; } + IEnumerable GroupedAccountCalendars { get; } +} + +public interface IShellViewModel +{ + WinoApplicationMode CurrentMode { get; } + IShellClient CurrentClient { get; } + MenuItemCollection? CurrentMenuItems { get; } + object? SelectedMenuItem { get; set; } + + void SetCurrentMode(WinoApplicationMode mode); + IShellClient GetClient(WinoApplicationMode mode); +} + +public interface IShellHost +{ + bool HasShellContent { get; } + + void ActivateMode(WinoApplicationMode mode, bool isInitialActivation); +} diff --git a/Wino.Core.Domain/MenuItems/NewCalendarEventMenuItem.cs b/Wino.Core.Domain/MenuItems/NewCalendarEventMenuItem.cs new file mode 100644 index 00000000..8fe87fec --- /dev/null +++ b/Wino.Core.Domain/MenuItems/NewCalendarEventMenuItem.cs @@ -0,0 +1,3 @@ +namespace Wino.Core.Domain.MenuItems; + +public sealed class NewCalendarEventMenuItem : NewMailMenuItem { } diff --git a/Wino.Core.Domain/Models/Navigation/ShellModeActivationContext.cs b/Wino.Core.Domain/Models/Navigation/ShellModeActivationContext.cs new file mode 100644 index 00000000..5c87ca2c --- /dev/null +++ b/Wino.Core.Domain/Models/Navigation/ShellModeActivationContext.cs @@ -0,0 +1,6 @@ +namespace Wino.Core.Domain.Models.Navigation; + +public sealed class ShellModeActivationContext +{ + public bool IsInitialActivation { get; init; } +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 08a75252..8d87609c 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -952,7 +952,6 @@ "SystemFolderConfigSetupSuccess_Title": "System Folders Setup", "SystemTrayMenu_ShowWino": "Open Wino Mail", "SystemTrayMenu_ShowWinoCalendar": "Open Wino Calendar", - "SystemTrayMenu_ShowWinoContacts": "Open Wino Contacts", "SystemTrayMenu_ExitWino": "Exit", "TestingImapConnectionMessage": "Testing server connection...", "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", diff --git a/Wino.Mail.ViewModels/MailAppShellViewModel.cs b/Wino.Mail.ViewModels/MailAppShellViewModel.cs index 277377d2..39e534ab 100644 --- a/Wino.Mail.ViewModels/MailAppShellViewModel.cs +++ b/Wino.Mail.ViewModels/MailAppShellViewModel.cs @@ -29,6 +29,7 @@ using Wino.Messaging.UI; namespace Wino.Mail.ViewModels; public partial class MailAppShellViewModel : MailBaseViewModel, + IMailShellClient, IRecipient, IRecipient, IRecipient, @@ -67,6 +68,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel, public IStatePersistanceService StatePersistenceService { get; } public IPreferencesService PreferencesService { get; } public INavigationService NavigationService { get; } + public WinoApplicationMode Mode => WinoApplicationMode.Mail; + public bool HandlesNavigationSelection => true; + public IMenuItem CreatePrimaryMenuItem => CreateMailMenuItem; private readonly IFolderService _folderService; private readonly IConfigurationService _configurationService; @@ -227,10 +231,14 @@ public partial class MailAppShellViewModel : MailBaseViewModel, { base.OnNavigatedTo(mode, parameters); + var activationContext = parameters as ShellModeActivationContext; + var isModeResetActivation = activationContext != null; + var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true; + PreferencesService.PreferenceChanged -= PreferencesServiceChanged; PreferencesService.PreferenceChanged += PreferencesServiceChanged; - if (mode == NavigationMode.Back) + if (mode == NavigationMode.Back && !isModeResetActivation) { // Preserve current mail/folder selection and active rendering page when // switching back from Calendar mode. Recreating menu/folder state here @@ -252,13 +260,16 @@ public partial class MailAppShellViewModel : MailBaseViewModel, await ProcessLaunchOptionsAsync(); await ValidateWebView2RuntimeAsync(); - if (!Debugger.IsAttached) + if (shouldRunStartupFlows && !Debugger.IsAttached) { await ForceAllAccountSynchronizationsAsync(); } - await ShowWhatIsNewIfNeededAsync(); - await MakeSureEnableStartupLaunchAsync(); + if (shouldRunStartupFlows) + { + await ShowWhatIsNewIfNeededAsync(); + await MakeSureEnableStartupLaunchAsync(); + } } private async Task ValidateWebView2RuntimeAsync() @@ -1258,6 +1269,18 @@ public partial class MailAppShellViewModel : MailBaseViewModel, } }); } + + void IShellClient.Activate(ShellModeActivationContext activationContext) + => OnNavigatedTo(NavigationMode.New, activationContext); + + void IShellClient.Deactivate() + => OnNavigatedFrom(NavigationMode.New, null!); + + Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem) + => MenuItemInvokedOrSelectedAsync(menuItem); + + Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem) + => menuItem == null ? Task.CompletedTask : MenuItemInvokedOrSelectedAsync(menuItem); } diff --git a/Wino.Mail.WinUI/App.xaml b/Wino.Mail.WinUI/App.xaml index ddc385cd..e6f54ed8 100644 --- a/Wino.Mail.WinUI/App.xaml +++ b/Wino.Mail.WinUI/App.xaml @@ -3,6 +3,8 @@ x:Class="Wino.Mail.WinUI.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:coreControls="using:Wino.Mail.WinUI.Controls" + xmlns:coreStyles="using:Wino.Mail.WinUI.Styles" xmlns:local="using:Wino.Mail.WinUI" xmlns:styles="using:Wino.Styles" xmlns:uwp="using:Wino.Mail.WinUI"> @@ -10,7 +12,101 @@ - + + + + + + + + + + + + + + + + + + 0 + + + + + + 24,24,24,24 + + + + + + + + + + + + + + 4 + + + diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index c920308f..f2637826 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -24,6 +24,7 @@ using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Synchronization; using Wino.Mail.Services; +using Wino.Mail.WinUI.ViewModels; using Wino.Mail.ViewModels; using Wino.Mail.ViewModels.Data; using Wino.Mail.WinUI.Activation; @@ -142,6 +143,13 @@ public partial class App : WinoApplication, { services.AddSingleton(typeof(MailAppShellViewModel)); services.AddSingleton(typeof(CalendarAppShellViewModel)); + services.AddSingleton(typeof(ContactsShellClient)); + services.AddSingleton(typeof(WinoAppShellViewModel)); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); services.AddTransient(typeof(MailListPageViewModel)); services.AddTransient(typeof(MailRenderingPageViewModel)); diff --git a/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml b/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml index 14b7381a..b0cac0d9 100644 --- a/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml +++ b/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml @@ -3,7 +3,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:CommunityToolkit.WinUI.Controls" - xmlns:domain="using:Wino.Core.Domain"> + xmlns:domain="using:Wino.Core.Domain" + Loaded="ControlLoaded" + Unloaded="ControlUnloaded"> (); InitializeComponent(); - - Loaded += ControlLoaded; - Unloaded += ControlUnloaded; } private void ControlLoaded(object sender, RoutedEventArgs e) @@ -34,7 +31,6 @@ public sealed partial class AppModeFooterSwitcherControl : UserControl { _statePersistenceService.StatePropertyChanged -= StatePropertyChanged; } - private void StatePropertyChanged(object? sender, string propertyName) { if (propertyName != nameof(IStatePersistanceService.ApplicationMode)) diff --git a/Wino.Mail.WinUI/CoreGeneric.xaml b/Wino.Mail.WinUI/CoreGeneric.xaml index c7042de3..7a55851f 100644 --- a/Wino.Mail.WinUI/CoreGeneric.xaml +++ b/Wino.Mail.WinUI/CoreGeneric.xaml @@ -9,13 +9,13 @@ - - - - - - - + + + + + + + diff --git a/Wino.Mail.WinUI/Properties/launchSettings.json b/Wino.Mail.WinUI/Properties/launchSettings.json index 4b52f706..14643837 100644 --- a/Wino.Mail.WinUI/Properties/launchSettings.json +++ b/Wino.Mail.WinUI/Properties/launchSettings.json @@ -2,7 +2,8 @@ "profiles": { "Wino.Mail.WinUI (Package)": { "commandName": "MsixPackage", - "doNotLaunchApp": false + "doNotLaunchApp": false, + "nativeDebugging": true }, "Wino.Mail.WinUI (Unpackaged)": { "commandName": "Project" diff --git a/Wino.Mail.WinUI/Selectors/NavigationMenuTemplateSelector.cs b/Wino.Mail.WinUI/Selectors/NavigationMenuTemplateSelector.cs index 4dce3d61..97167fbf 100644 --- a/Wino.Mail.WinUI/Selectors/NavigationMenuTemplateSelector.cs +++ b/Wino.Mail.WinUI/Selectors/NavigationMenuTemplateSelector.cs @@ -21,13 +21,16 @@ public partial class NavigationMenuTemplateSelector : DataTemplateSelector public DataTemplate CreateNewFolderTemplate { get; set; } = null!; public DataTemplate SeperatorTemplate { get; set; } = null!; public DataTemplate NewMailTemplate { get; set; } = null!; + public DataTemplate CalendarNewEventTemplate { get; set; } = null!; public DataTemplate CategoryItemsTemplate { get; set; } = null!; public DataTemplate FixAuthenticationIssueTemplate { get; set; } = null!; public DataTemplate FixMissingFolderConfigTemplate { get; set; } = null!; protected override DataTemplate SelectTemplateCore(object item) { - if (item is NewMailMenuItem) + if (item is NewCalendarEventMenuItem) + return CalendarNewEventTemplate; + else if (item is NewMailMenuItem) return NewMailTemplate; else if (item is ContactsMenuItem) return ContactsMenuItemTemplate; diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index d1e9ebf0..07e43fdf 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -15,11 +15,11 @@ using Wino.Mail.WinUI; using Wino.Mail.WinUI.Interfaces; using Wino.Mail.WinUI.Models; using Wino.Mail.WinUI.Services; +using Wino.Mail.WinUI.Views; using Wino.Mail.WinUI.Views.Calendar; using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Navigation; -using Wino.Mail.WinUI.Views.Contacts; using Wino.Views; using Wino.Views.Account; using Wino.Views.Mail; @@ -33,6 +33,7 @@ public class NavigationService : NavigationServiceBase, INavigationService private readonly IStatePersistanceService _statePersistanceService; private readonly IDispatcher _dispatcher; private readonly IWinoWindowManager _windowManager; + private NavigationTransitionInfo? _pendingInnerShellTransition; private WinoPage[] _renderingPageTypes = new WinoPage[] { @@ -192,48 +193,33 @@ public class NavigationService : NavigationServiceBase, INavigationService if (coreFrame == null) return false; var currentMode = _statePersistanceService.ApplicationMode; + var isInitialShellNavigation = coreFrame.Content is not IShellHost; // Update the application mode in state persistence service _statePersistanceService.ApplicationMode = mode; _statePersistanceService.AppModeTitle = GetApplicationModeTitle(mode); - var targetPageType = mode switch - { - WinoApplicationMode.Calendar => typeof(CalendarAppShell), - WinoApplicationMode.Contacts => typeof(ContactsAppShell), - _ => typeof(MailAppShell) - }; - var currentPageType = coreFrame.Content?.GetType(); - var transitionInfo = GetApplicationModeTransitionInfo(currentMode, mode); - - // If already on the target page, do nothing - if (currentPageType == targetPageType) + if (coreFrame.Content is IShellHost activeShell && activeShell.HasShellContent && currentMode == mode) return true; - // Check if we can go back to the target page - if (coreFrame.CanGoBack && coreFrame.BackStack.Count > 0) + _pendingInnerShellTransition = isInitialShellNavigation + ? null + : GetApplicationModeTransitionInfo(currentMode, mode); + + if (coreFrame.Content is not IShellHost) { - var previousPage = coreFrame.BackStack[coreFrame.BackStack.Count - 1]; - if (previousPage.SourcePageType == targetPageType) - { - coreFrame.GoBack(transitionInfo); - return true; - } + coreFrame.BackStack.Clear(); + coreFrame.ForwardStack.Clear(); + coreFrame.Navigate(typeof(WinoAppShell), null, new SuppressNavigationTransitionInfo()); } - // Check if we can go forward to the target page - if (coreFrame.CanGoForward && coreFrame.ForwardStack.Count > 0) + if (coreFrame.Content is IShellHost shell) { - var nextPage = coreFrame.ForwardStack[coreFrame.ForwardStack.Count - 1]; - if (nextPage.SourcePageType == targetPageType) - { - coreFrame.GoForward(); - return true; - } + shell.ActivateMode(mode, isInitialShellNavigation); + return true; } - // Navigate to the target page only if it's not in the navigation stack - coreFrame.Navigate(targetPageType, null, transitionInfo); + _pendingInnerShellTransition = null; return true; } @@ -300,7 +286,7 @@ public class NavigationService : NavigationServiceBase, INavigationService } } - return innerShellFrame.Navigate(pageType, parameter); + return NavigateInnerShellFrame(innerShellFrame, pageType, parameter, transition); } else { @@ -318,14 +304,14 @@ public class NavigationService : NavigationServiceBase, INavigationService return true; } - var transitionInfo = GetNavigationTransitionInfo(transition); - // This page must be opened in the Frame placed in MailListingPage. if (isMailListingPageActive && frame == NavigationReferenceFrame.RenderingFrame) { var listingFrame = GetCoreFrameInternal(NavigationReferenceFrame.RenderingFrame); if (listingFrame == null) return false; + var transitionInfo = GetNavigationTransitionInfo(transition); + // Active page is mail list page and we are opening a mail item. // No navigation needed, just refresh the rendered mail item. if (listingFrame.Content != null @@ -361,7 +347,7 @@ public class NavigationService : NavigationServiceBase, INavigationService if ((currentFrameType != null && currentFrameType != pageType) || currentFrameType == null) { - return innerShellFrame.Navigate(pageType, parameter, transitionInfo); + return NavigateInnerShellFrame(innerShellFrame, pageType, parameter, transition); } } } @@ -429,6 +415,24 @@ public class NavigationService : NavigationServiceBase, INavigationService return new LoadCalendarMessage(targetDate, initiative); } + private bool NavigateInnerShellFrame(Frame frame, Type pageType, object? parameter, NavigationTransitionType transition) + { + var transitionInfo = ConsumeInnerShellTransitionOrDefault(transition); + return frame.Navigate(pageType, parameter, transitionInfo); + } + + private NavigationTransitionInfo ConsumeInnerShellTransitionOrDefault(NavigationTransitionType transition) + { + if (_pendingInnerShellTransition != null) + { + var transitionInfo = _pendingInnerShellTransition; + _pendingInnerShellTransition = null; + return transitionInfo; + } + + return GetNavigationTransitionInfo(transition); + } + public void GoBack(Core.Domain.Enums.NavigationTransitionEffect slideEffect = Core.Domain.Enums.NavigationTransitionEffect.FromRight) => ExecuteOnNavigationThread(() => GoBackInternal(slideEffect)); diff --git a/Wino.Mail.WinUI/ShellWindow.xaml b/Wino.Mail.WinUI/ShellWindow.xaml index 3ca32d25..99c29e40 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml +++ b/Wino.Mail.WinUI/ShellWindow.xaml @@ -116,7 +116,7 @@ - + diff --git a/Wino.Mail.WinUI/ShellWindow.xaml.cs b/Wino.Mail.WinUI/ShellWindow.xaml.cs index fec3a812..46b5ea63 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml.cs +++ b/Wino.Mail.WinUI/ShellWindow.xaml.cs @@ -16,9 +16,9 @@ using Wino.Core.Domain.Models.Synchronization; using Wino.Extensions; using Wino.Mail.WinUI.Activation; using Wino.Mail.WinUI.Interfaces; +using Wino.Mail.WinUI.Views; using Wino.Messaging.Client.Shell; using Wino.Messaging.UI; -using Wino.Views; using WinUIEx; namespace Wino.Mail.WinUI; @@ -139,12 +139,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, _ = StartCalendarReminderServerAsync(); } - // Mail shell has shell content only for mail list page - // Thus, we check if the current content is MailAppShell - - if (sender is Frame mainFrame && mainFrame.Content is MailAppShell mailAppShellPage) - ShellTitleBar.Content = mailAppShellPage.TopShellContent; - else if (e.Content is BasePage basePage) + if (e.Content is BasePage basePage) ShellTitleBar.Content = basePage.ShellContent; } @@ -172,9 +167,9 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, public void Receive(TitleBarShellContentUpdated message) { - if (MainShellFrame.Content is MailAppShell shellPage) + if (MainShellFrame.Content is WinoAppShell shellPage) { - ShellTitleBar.Content = shellPage.TopShellContent; + ShellTitleBar.Content = shellPage.ShellContent; } } diff --git a/Wino.Mail.WinUI/ViewModels/ContactsShellClient.cs b/Wino.Mail.WinUI/ViewModels/ContactsShellClient.cs new file mode 100644 index 00000000..f17cefd0 --- /dev/null +++ b/Wino.Mail.WinUI/ViewModels/ContactsShellClient.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.MenuItems; +using Wino.Core.Domain.Models; +using Wino.Core.Domain.Models.Navigation; +using Wino.Core.ViewModels; + +namespace Wino.Mail.WinUI.ViewModels; + +public sealed class ContactsShellClient(INavigationService navigationService) : CoreBaseViewModel, IShellClient +{ + public WinoApplicationMode Mode => WinoApplicationMode.Contacts; + public MenuItemCollection? MenuItems { get; private set; } + public object? SelectedMenuItem { get; set; } + public bool HandlesNavigationSelection => false; + + protected override void OnDispatcherAssigned() + { + base.OnDispatcherAssigned(); + MenuItems ??= new MenuItemCollection(Dispatcher); + } + + public void Activate(ShellModeActivationContext activationContext) + { + OnNavigatedTo(NavigationMode.New, activationContext); + navigationService.Navigate(WinoPage.ContactsPage, null, NavigationReferenceFrame.InnerShellFrame); + } + + public void Deactivate() + { + OnNavigatedFrom(NavigationMode.New, null!); + } + + public Task HandleNavigationItemInvokedAsync(IMenuItem? menuItem) => Task.CompletedTask; + + public Task HandleNavigationSelectionChangedAsync(IMenuItem? menuItem) => Task.CompletedTask; +} diff --git a/Wino.Mail.WinUI/ViewModels/WinoAppShellViewModel.cs b/Wino.Mail.WinUI/ViewModels/WinoAppShellViewModel.cs new file mode 100644 index 00000000..9cecc440 --- /dev/null +++ b/Wino.Mail.WinUI/ViewModels/WinoAppShellViewModel.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.MenuItems; +using Wino.Core.ViewModels; + +namespace Wino.Mail.WinUI.ViewModels; + +public sealed class WinoAppShellViewModel : CoreBaseViewModel, IShellViewModel +{ + private readonly Dictionary _shellClients; + private WinoApplicationMode _currentMode; + + public WinoAppShellViewModel(IMailShellClient mailClient, + ICalendarShellClient calendarClient, + IEnumerable shellClients, + IPreferencesService preferencesService, + IStatePersistanceService statePersistenceService, + INavigationService navigationService) + { + MailClient = mailClient; + CalendarClient = calendarClient; + PreferencesService = preferencesService; + StatePersistenceService = statePersistenceService; + NavigationService = navigationService; + + _shellClients = shellClients.ToDictionary(client => client.Mode); + + foreach (var client in _shellClients.Values) + { + client.PropertyChanged += ChildPropertyChanged; + } + + StatePersistenceService.StatePropertyChanged += StatePersistenceServiceChanged; + } + + public IMailShellClient MailClient { get; } + public ICalendarShellClient CalendarClient { get; } + public IPreferencesService PreferencesService { get; } + public IStatePersistanceService StatePersistenceService { get; } + public INavigationService NavigationService { get; } + + public WinoApplicationMode CurrentMode + { + get => _currentMode; + private set + { + if (SetProperty(ref _currentMode, value)) + { + OnPropertyChanged(nameof(CurrentClient)); + OnPropertyChanged(nameof(CurrentMenuItems)); + OnPropertyChanged(nameof(IsMailMode)); + OnPropertyChanged(nameof(IsCalendarMode)); + OnPropertyChanged(nameof(IsContactsMode)); + OnPropertyChanged(nameof(SelectedMenuItem)); + } + } + } + + public IShellClient CurrentClient => GetClient(CurrentMode); + public bool IsMailMode => CurrentMode == WinoApplicationMode.Mail; + public bool IsCalendarMode => CurrentMode == WinoApplicationMode.Calendar; + public bool IsContactsMode => CurrentMode == WinoApplicationMode.Contacts; + public MenuItemCollection? CurrentMenuItems => CurrentClient.MenuItems; + + public object? SelectedMenuItem + { + get => CurrentClient.SelectedMenuItem; + set + { + if (!ReferenceEquals(CurrentClient.SelectedMenuItem, value)) + { + CurrentClient.SelectedMenuItem = value; + OnPropertyChanged(); + } + } + } + + protected override void OnDispatcherAssigned() + { + base.OnDispatcherAssigned(); + + foreach (var client in _shellClients.Values) + { + client.Dispatcher = Dispatcher; + } + + OnPropertyChanged(nameof(CurrentMenuItems)); + } + + public override void OnNavigatedTo(Core.Domain.Models.Navigation.NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + CurrentMode = StatePersistenceService.ApplicationMode; + } + + public IShellClient GetClient(WinoApplicationMode mode) + => _shellClients[mode]; + + public void SetCurrentMode(WinoApplicationMode mode) + { + CurrentMode = mode; + OnPropertyChanged(nameof(CurrentMenuItems)); + } + + private void ChildPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (ReferenceEquals(sender, CurrentClient)) + { + if (e.PropertyName == nameof(IShellClient.SelectedMenuItem)) + { + OnPropertyChanged(nameof(SelectedMenuItem)); + } + + if (e.PropertyName == nameof(IShellClient.MenuItems)) + { + OnPropertyChanged(nameof(CurrentMenuItems)); + } + } + } + + private void StatePersistenceServiceChanged(object? sender, string propertyName) + { + if (propertyName == nameof(IStatePersistanceService.ApplicationMode)) + { + SetCurrentMode(StatePersistenceService.ApplicationMode); + } + } +} diff --git a/Wino.Mail.WinUI/Views/Abstract/WinoAppShellAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/WinoAppShellAbstract.cs new file mode 100644 index 00000000..06dd2a36 --- /dev/null +++ b/Wino.Mail.WinUI/Views/Abstract/WinoAppShellAbstract.cs @@ -0,0 +1,11 @@ +using Wino.Mail.WinUI.ViewModels; + +namespace Wino.Mail.WinUI.Views.Abstract; + +public abstract class WinoAppShellAbstract : BasePage +{ + protected WinoAppShellAbstract() + { + NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Enabled; + } +} diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml index 6ec62f7d..3bdcbe55 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml @@ -20,7 +20,7 @@ mc:Ignorable="d"> - + diff --git a/Wino.Mail.WinUI/Views/WinoAppShell.xaml b/Wino.Mail.WinUI/Views/WinoAppShell.xaml new file mode 100644 index 00000000..4761a4ac --- /dev/null +++ b/Wino.Mail.WinUI/Views/WinoAppShell.xaml @@ -0,0 +1,730 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs b/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs new file mode 100644 index 00000000..18c5b7d3 --- /dev/null +++ b/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs @@ -0,0 +1,679 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.WinUI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Wino.Calendar.Controls; +using Wino.Calendar.ViewModels; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models; +using Wino.Core.Domain.Models.Folders; +using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Models.Navigation; +using Wino.Mail.ViewModels; +using Wino.Mail.ViewModels.Data; +using Wino.Mail.WinUI.Controls; +using Wino.MenuFlyouts; +using Wino.MenuFlyouts.Context; +using Wino.Messaging.Client.Accounts; +using Wino.Messaging.Client.Calendar; +using Wino.Messaging.Client.Mails; +using Wino.Messaging.Client.Shell; + +namespace Wino.Mail.WinUI.Views; + +public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, + IShellHost, + IRecipient, + IRecipient, + IRecipient, + IRecipient +{ + private const string StateHorizontalCalendar = "HorizontalCalendar"; + private const string StateVerticalCalendar = "VerticalCalendar"; + private const string StateDefaultShellContent = "DefaultShellContentState"; + private const string StateEventDetailsContent = "EventDetailsContentState"; + private WinoApplicationMode? _activeMode; + + public WinoAppShell() + { + InitializeComponent(); + + ViewModel.MailClient.PropertyChanged += MailClientPropertyChanged; + ViewModel.CalendarClient.PropertyChanged += CalendarClientPropertyChanged; + ViewModel.StatePersistenceService.StatePropertyChanged += StatePersistenceServiceChanged; + CalendarTypeSelector.RegisterPropertyChangedCallback(WinoCalendarTypeSelectorControl.SelectedTypeProperty, CalendarTypeSelectorSelectedTypeChanged); + + InitializeCalendarControls(); + ManageCalendarDisplayType(ViewModel.CalendarClient.StatePersistenceService.CalendarDisplayType); + UpdateEventDetailsVisualState(); + ApplyTitleBarContent(); + } + + public bool HasShellContent => InnerShellFrame.Content != null; + + public Frame GetShellFrame() => InnerShellFrame; + + public void ActivateMode(WinoApplicationMode mode, bool isInitialActivation) + { + if (_activeMode == mode && InnerShellFrame.Content != null) + return; + + DeactivateCurrentMode(); + + _activeMode = mode; + ViewModel.SetCurrentMode(mode); + + RefreshNavigationViewBindings(syncMailSelection: mode != WinoApplicationMode.Mail); + + //InnerShellFrame.IsNavigationStackEnabled = mode == WinoApplicationMode.Calendar; + //InnerShellFrame.BackStack.Clear(); + //InnerShellFrame.ForwardStack.Clear(); + + ApplyModeLayout(); + UpdateTitleBarSubtitle(); + + var activationContext = new ShellModeActivationContext + { + IsInitialActivation = isInitialActivation + }; + + ViewModel.CurrentClient.Activate(activationContext); + + ApplyTitleBarContent(); + } + + protected override void OnNavigatedFrom(NavigationEventArgs e) + { + base.OnNavigatedFrom(e); + DeactivateCurrentMode(); + Bindings.StopTracking(); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + UpdateNavigationPaneLayout(navigationView.DisplayMode); + RefreshNavigationViewBindings(); + RefreshCalendarControls(); + + if (_activeMode == null) + { + ActivateMode(ViewModel.StatePersistenceService.ApplicationMode, true); + } + } + + private void ApplyModeLayout() + { + var isCalendarMode = ViewModel.IsCalendarMode; + + CalendarShellContentRoot.Visibility = isCalendarMode ? Visibility.Visible : Visibility.Collapsed; + DynamicPageShellContentPresenter.Visibility = isCalendarMode ? Visibility.Collapsed : Visibility.Visible; + + RefreshCalendarControls(); + ManageCalendarDisplayType(ViewModel.CalendarClient.StatePersistenceService.CalendarDisplayType); + UpdateEventDetailsVisualState(); + UpdateTitleBarSubtitle(); + UpdateNavigationPaneLayout(navigationView.DisplayMode); + ApplyTitleBarContent(); + } + + private void DeactivateCurrentMode() + { + if (_activeMode == WinoApplicationMode.Mail) + { + ViewModel.MailClient.Deactivate(); + } + else if (_activeMode == WinoApplicationMode.Calendar) + { + ViewModel.CalendarClient.Deactivate(); + } + else if (_activeMode == WinoApplicationMode.Contacts) + { + ViewModel.CurrentClient.Deactivate(); + } + } + + private void ApplyTitleBarContent() + { + if (ViewModel.IsCalendarMode) + { + CalendarShellContentRoot.Visibility = Visibility.Visible; + DynamicPageShellContentPresenter.Visibility = Visibility.Collapsed; + return; + } + + CalendarShellContentRoot.Visibility = Visibility.Collapsed; + DynamicPageShellContentPresenter.Visibility = Visibility.Visible; + DynamicPageShellContentPresenter.Content = InnerShellFrame.Content is BasePage page ? page.ShellContent : null; + } + + private void UpdateTitleBarSubtitle() + { + if (ViewModel.IsContactsMode) + { + ViewModel.StatePersistenceService.CoreWindowTitle = string.Empty; + return; + } + + if (ViewModel.IsCalendarMode) + { + ViewModel.StatePersistenceService.CoreWindowTitle = ViewModel.CalendarClient.HighlightedDateRange?.ToString() ?? string.Empty; + return; + } + + ViewModel.StatePersistenceService.CoreWindowTitle = string.Empty; + } + + private void ManageCalendarDisplayType(Core.Domain.Enums.CalendarDisplayType displayType) + { + DayHeaderNavigationItemsFlipView.DisplayType = displayType; + + if (CalendarTypeSelector.SelectedType != displayType) + { + CalendarTypeSelector.SelectedType = displayType; + } + + VisualStateManager.GoToState(this, displayType == Core.Domain.Enums.CalendarDisplayType.Month + ? StateVerticalCalendar + : StateHorizontalCalendar, false); + } + + private void InitializeCalendarControls() + { + CalendarTypeSelector.TodayClickedCommand = ViewModel.CalendarClient.TodayClickedCommand; + CalendarView.DateClickedCommand = ViewModel.CalendarClient.DateClickedCommand; + DayHeaderNavigationItemsFlipView.ItemsSource = ViewModel.CalendarClient.DateNavigationHeaderItems; + CalendarHostListView.ItemsSource = ViewModel.CalendarClient.GroupedAccountCalendars; + + RefreshCalendarControls(); + } + + private void RefreshCalendarControls() + { + DayHeaderNavigationItemsFlipView.ItemsSource = ViewModel.CalendarClient.DateNavigationHeaderItems; + DayHeaderNavigationItemsFlipView.SelectedIndex = ViewModel.CalendarClient.SelectedDateNavigationHeaderIndex; + CalendarTypeSelector.DisplayDayCount = ViewModel.CalendarClient.StatePersistenceService.DayDisplayCount; + CalendarView.HighlightedDateRange = ViewModel.CalendarClient.HighlightedDateRange; + CalendarHostListView.ItemsSource = ViewModel.CalendarClient.GroupedAccountCalendars; + } + + private void RefreshNavigationViewBindings(bool syncMailSelection = true) + { + navigationView.MenuItemsSource = ViewModel.CurrentMenuItems; + + navigationView.SelectionChanged -= MenuSelectionChanged; + navigationView.SelectedItem = ViewModel.CurrentClient.HandlesNavigationSelection && syncMailSelection + ? ViewModel.SelectedMenuItem + : null; + navigationView.SelectionChanged += MenuSelectionChanged; + } + + private void UpdateEventDetailsVisualState() + { + VisualStateManager.GoToState(this, + ViewModel.StatePersistenceService.IsEventDetailsVisible ? StateEventDetailsContent : StateDefaultShellContent, + false); + } + + private void CalendarTypeSelectorSelectedTypeChanged(DependencyObject sender, DependencyProperty dp) + { + var selectedType = CalendarTypeSelector.SelectedType; + + if (ViewModel.CalendarClient.StatePersistenceService.CalendarDisplayType != selectedType) + { + ViewModel.CalendarClient.StatePersistenceService.CalendarDisplayType = selectedType; + } + } + + private void PreviousDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoPreviousDateRequestedMessage()); + + private void NextDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoNextDateRequestedMessage()); + + public void Receive(CalendarDisplayTypeChangedMessage message) => ManageCalendarDisplayType(message.NewDisplayType); + + private async void NavigationViewItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) + { + if (ViewModel.IsCalendarMode) + { + if (args.InvokedItemContainer is FrameworkElement { DataContext: IMenuItem menuItem }) + { + await ViewModel.CalendarClient.HandleNavigationItemInvokedAsync(menuItem); + } + + return; + } + + if (args.InvokedItemContainer is WinoNavigationViewItem winoNavigationViewItem) + { + if (winoNavigationViewItem.SelectsOnInvoked) + return; + + await ViewModel.CurrentClient.HandleNavigationItemInvokedAsync(winoNavigationViewItem.DataContext as IMenuItem); + } + } + + private async void MenuSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) + { + if (!ViewModel.IsMailMode) + return; + + if (args.SelectedItem is IMenuItem invokedMenuItem) + { + await ViewModel.CurrentClient.HandleNavigationSelectionChangedAsync(invokedMenuItem); + } + } + + public void Receive(AccountMenuItemExtended message) + { + if (!ViewModel.IsMailMode) + return; + + _ = DispatcherQueue.EnqueueAsync(async () => + { + if (message.FolderId == default) + return; + + if (ViewModel.MailClient.MenuItems!.TryGetFolderMenuItem(message.FolderId, out IBaseFolderMenuItem foundMenuItem)) + { + foundMenuItem.Expand(); + await ViewModel.MailClient.NavigateFolderAsync(foundMenuItem); + navigationView.SelectedItem = foundMenuItem; + + if (message.NavigateMailItem != null) + { + WeakReferenceMessenger.Default.Send(new MailItemNavigationRequested(message.NavigateMailItem.UniqueId, ScrollToItem: true)); + } + + return; + } + + if (message.NavigateMailItem == null) + return; + + if (ViewModel.MailClient.MenuItems!.TryGetAccountMenuItem(message.NavigateMailItem.AssignedAccount.Id, out IAccountMenuItem accountMenuItem)) + { + await ViewModel.MailClient.ChangeLoadedAccountAsync(accountMenuItem, navigateInbox: false); + + if (ViewModel.MailClient.MenuItems!.TryGetFolderMenuItem(message.FolderId, out IBaseFolderMenuItem accountFolderMenuItem)) + { + accountFolderMenuItem.Expand(); + await ViewModel.MailClient.NavigateFolderAsync(accountFolderMenuItem); + navigationView.SelectedItem = accountFolderMenuItem; + WeakReferenceMessenger.Default.Send(new MailItemNavigationRequested(message.NavigateMailItem.UniqueId, ScrollToItem: true)); + } + } + }); + } + + public void Receive(NavigateMailFolderEvent message) + { + if (!ViewModel.IsMailMode || message.BaseFolderMenuItem == null) + return; + + if (navigationView.SelectedItem != message.BaseFolderMenuItem) + { + var navigateFolderArgs = new NavigateMailFolderEventArgs(message.BaseFolderMenuItem, message.FolderInitLoadAwaitTask); + + ViewModel.NavigationService.Navigate(WinoPage.MailListPage, navigateFolderArgs, NavigationReferenceFrame.InnerShellFrame); + + navigationView.SelectionChanged -= MenuSelectionChanged; + navigationView.SelectedItem = message.BaseFolderMenuItem; + navigationView.SelectionChanged += MenuSelectionChanged; + } + else + { + message.FolderInitLoadAwaitTask?.TrySetResult(true); + } + } + + private void ShellFrameContentNavigated(object sender, NavigationEventArgs e) + { + ApplyTitleBarContent(); + + if (ViewModel.IsMailMode) + { + RefreshNavigationViewBindings(); + } + } + + private async void MenuItemContextRequested(UIElement sender, ContextRequestedEventArgs args) + { + if (!ViewModel.IsMailMode) + return; + + if (sender is WinoNavigationViewItem menuItem && + menuItem.DataContext is IBaseFolderMenuItem baseFolderMenuItem && + baseFolderMenuItem.IsMoveTarget && + args.TryGetPosition(sender, out Point p)) + { + args.Handled = true; + + var source = new TaskCompletionSource(); + var actions = ViewModel.MailClient.GetFolderContextMenuActions(baseFolderMenuItem); + var flyout = new FolderOperationFlyout(actions, source); + + flyout.ShowAt(menuItem, new FlyoutShowOptions + { + ShowMode = FlyoutShowMode.Standard, + Position = new Point(p.X + 30, p.Y - 20) + }); + + var operation = await source.Task; + flyout.Dispose(); + + if (operation != null) + { + await ViewModel.MailClient.PerformFolderOperationAsync(operation.Operation, baseFolderMenuItem); + } + } + } + + public void Receive(CreateNewMailWithMultipleAccountsRequested message) + { + if (!ViewModel.IsMailMode) + return; + + var container = navigationView.ContainerFromMenuItem(ViewModel.MailClient.CreatePrimaryMenuItem); + var flyout = new AccountSelectorFlyout(message.AllAccounts, ViewModel.MailClient.CreateNewMailForAsync); + + flyout.ShowAt(container, new FlyoutShowOptions + { + ShowMode = FlyoutShowMode.Auto, + Placement = FlyoutPlacementMode.Right + }); + } + + private void NavigationPaneOpening(NavigationView sender, object args) + { + if (!ViewModel.IsMailMode) + return; + + if (sender.DisplayMode == NavigationViewDisplayMode.Minimal && sender.SelectedItem is IFolderMenuItem selectedFolderMenuItem) + { + selectedFolderMenuItem.Expand(); + } + } + + private void NavigationViewDisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args) + => UpdateNavigationPaneLayout(args.DisplayMode); + + private void MailClientPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IShellClient.SelectedMenuItem) && ViewModel.IsMailMode) + { + navigationView.SelectionChanged -= MenuSelectionChanged; + navigationView.SelectedItem = ViewModel.MailClient.SelectedMenuItem; + navigationView.SelectionChanged += MenuSelectionChanged; + } + } + + private void CalendarClientPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ICalendarShellClient.DateNavigationHeaderItems)) + { + DayHeaderNavigationItemsFlipView.ItemsSource = ViewModel.CalendarClient.DateNavigationHeaderItems; + return; + } + + if (e.PropertyName == nameof(ICalendarShellClient.SelectedDateNavigationHeaderIndex)) + { + DayHeaderNavigationItemsFlipView.SelectedIndex = ViewModel.CalendarClient.SelectedDateNavigationHeaderIndex; + return; + } + + if (e.PropertyName == nameof(ICalendarShellClient.HighlightedDateRange)) + { + CalendarView.HighlightedDateRange = ViewModel.CalendarClient.HighlightedDateRange; + UpdateTitleBarSubtitle(); + } + } + + private void StatePersistenceServiceChanged(object? sender, string propertyName) + { + if (propertyName == nameof(IStatePersistanceService.CalendarDisplayType)) + { + ManageCalendarDisplayType(ViewModel.CalendarClient.StatePersistenceService.CalendarDisplayType); + return; + } + + if (propertyName == nameof(IStatePersistanceService.DayDisplayCount)) + { + CalendarTypeSelector.DisplayDayCount = ViewModel.CalendarClient.StatePersistenceService.DayDisplayCount; + return; + } + + if (propertyName == nameof(IStatePersistanceService.IsEventDetailsVisible)) + { + UpdateEventDetailsVisualState(); + } + } + + private void UpdateNavigationPaneLayout(NavigationViewDisplayMode displayMode) + { + if (ViewModel.IsCalendarMode) + { + PaneCustomContent.Visibility = displayMode == NavigationViewDisplayMode.Expanded && navigationView.IsPaneOpen + ? Visibility.Visible + : Visibility.Collapsed; + + InnerShellFrame.Margin = new Thickness(0); + return; + } + + PaneCustomContent.Visibility = Visibility.Collapsed; + InnerShellFrame.Margin = displayMode == NavigationViewDisplayMode.Minimal + ? new Thickness(7, 0, 0, 0) + : new Thickness(0); + } + + private async void OnPreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.KeyStatus.RepeatCount > 1 || ShouldIgnoreShortcut()) + return; + + var key = NormalizeKey(e.Key); + if (string.IsNullOrEmpty(key)) + return; + + var mode = ViewModel.CurrentMode; + var shortcutService = WinoApplication.Current.Services.GetRequiredService(); + var shortcut = await shortcutService.GetShortcutForKeyAsync(mode, key, GetCurrentModifierKeys()); + + if (shortcut == null) + return; + + var details = new KeyboardShortcutTriggerDetails + { + ShortcutId = shortcut.Id, + Mode = shortcut.Mode, + Action = shortcut.Action, + Key = shortcut.Key, + ModifierKeys = shortcut.ModifierKeys, + Sender = sender, + Origin = FocusManager.GetFocusedElement(XamlRoot) + }; + + await ViewModel.CurrentClient.KeyboardShortcutHook(details); + + if (InnerShellFrame.Content is BasePage activePage && activePage.AssociatedViewModel != null) + { + await activePage.AssociatedViewModel.KeyboardShortcutHook(details); + } + + if (details.Handled) + { + e.Handled = true; + } + } + + private bool ShouldIgnoreShortcut() + { + var focusedElement = FocusManager.GetFocusedElement(XamlRoot); + + if (focusedElement is TextBox or AutoSuggestBox or PasswordBox or RichEditBox or ComboBox) + return true; + + if (focusedElement is FrameworkElement frameworkElement) + { + var typeName = frameworkElement.GetType().Name; + if (typeName.Contains("WebView", StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + private async void ItemDroppedOnFolder(object sender, DragEventArgs e) + { + if (sender is WinoNavigationViewItem droppedContainer) + { + droppedContainer.IsDraggingItemOver = false; + + if (CanContinueDragDrop(droppedContainer, e) && droppedContainer.DataContext is IBaseFolderMenuItem draggingFolder) + { + var dragPackage = e.DataView.Properties[nameof(MailDragPackage)] as MailDragPackage; + if (dragPackage == null) + return; + + e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; + var mailCopies = ExtractMailCopies(dragPackage).ToList(); + await ViewModel.MailClient.PerformMoveOperationAsync(mailCopies, draggingFolder); + } + } + } + + private void ItemDragLeaveFromFolder(object sender, DragEventArgs e) + { + if (sender is WinoNavigationViewItem leavingContainer) + { + leavingContainer.IsDraggingItemOver = false; + } + } + + private bool CanContinueDragDrop(WinoNavigationViewItem interactingContainer, DragEventArgs args) + { + if (!ViewModel.IsMailMode || !args.DataView.Properties.ContainsKey(nameof(MailDragPackage))) + return false; + + var dragPackage = args.DataView.Properties[nameof(MailDragPackage)] as MailDragPackage; + if (dragPackage == null || !dragPackage.DraggingMails.Any()) + return false; + + if (interactingContainer.IsSelected) + return false; + + if (interactingContainer.DataContext is not IBaseFolderMenuItem folderMenuItem || !folderMenuItem.IsMoveTarget) + return false; + + var draggedAccountIds = folderMenuItem.HandlingFolders.Select(a => a.MailAccountId); + var draggedMails = ExtractMailCopies(dragPackage).ToList(); + + return draggedMails.Any() && draggedMails.Any(a => draggedAccountIds.Contains(a.AssignedAccount.Id)); + } + + private static IEnumerable ExtractMailCopies(MailDragPackage dragPackage) + { + foreach (var item in dragPackage.DraggingMails) + { + if (item is MailCopy mailCopy) + { + yield return mailCopy; + } + else if (item is MailItemViewModel singleMailItemViewModel) + { + yield return singleMailItemViewModel.MailCopy; + } + else if (item is ThreadMailItemViewModel threadViewModel) + { + foreach (var threadMail in threadViewModel.ThreadEmails) + { + yield return threadMail.MailCopy; + } + } + } + } + + private void ItemDragEnterOnFolder(object sender, DragEventArgs e) + { + if (sender is WinoNavigationViewItem droppedContainer && CanContinueDragDrop(droppedContainer, e)) + { + droppedContainer.IsDraggingItemOver = true; + + if (droppedContainer.DataContext is IBaseFolderMenuItem draggingFolder) + { + e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; + e.DragUIOverride.Caption = string.Format(Translator.DragMoveToFolderCaption, draggingFolder.FolderName); + } + } + } + + private static ModifierKeys GetCurrentModifierKeys() + { + var modifiers = ModifierKeys.None; + + if (Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Control).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down)) + modifiers |= ModifierKeys.Control; + if (Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Menu).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down)) + modifiers |= ModifierKeys.Alt; + if (Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Shift).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down)) + modifiers |= ModifierKeys.Shift; + if (Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.LeftWindows).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down) || + Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.RightWindows).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down)) + { + modifiers |= ModifierKeys.Windows; + } + + return modifiers; + } + + private static string NormalizeKey(Windows.System.VirtualKey key) + { + return key switch + { + Windows.System.VirtualKey.Control or + Windows.System.VirtualKey.LeftControl or + Windows.System.VirtualKey.RightControl or + Windows.System.VirtualKey.Menu or + Windows.System.VirtualKey.LeftMenu or + Windows.System.VirtualKey.RightMenu or + Windows.System.VirtualKey.Shift or + Windows.System.VirtualKey.LeftShift or + Windows.System.VirtualKey.RightShift or + Windows.System.VirtualKey.LeftWindows or + Windows.System.VirtualKey.RightWindows => string.Empty, + _ => key.ToString() + }; + } + + protected override void RegisterRecipients() + { + base.RegisterRecipients(); + + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + } + + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + } +}