diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs index 9c66f59c..c9593a8a 100644 --- a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs @@ -416,7 +416,15 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, return; } - pickedCalendar = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups); + var pickingResult = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups); + + if (pickingResult.ShouldNavigateToCalendarSettings) + { + NavigationService.Navigate(WinoPage.CalendarSettingsPage); + return; + } + + pickedCalendar = pickingResult.PickedCalendar; } if (pickedCalendar == null) diff --git a/Wino.Core.Domain/Interfaces/IMailDialogService.cs b/Wino.Core.Domain/Interfaces/IMailDialogService.cs index 807a473b..8d5ab945 100644 --- a/Wino.Core.Domain/Interfaces/IMailDialogService.cs +++ b/Wino.Core.Domain/Interfaces/IMailDialogService.cs @@ -20,7 +20,7 @@ public interface IMailDialogService : IDialogServiceBase // Custom dialogs Task ShowMoveMailFolderDialogAsync(List availableFolders); Task ShowAccountPickerDialogAsync(List availableAccounts); - Task ShowSingleCalendarPickerDialogAsync(List availableCalendarGroups); + Task ShowSingleCalendarPickerDialogAsync(List availableCalendarGroups); /// /// Displays a dialog to the user for reordering accounts. diff --git a/Wino.Core.Domain/Interfaces/IShellClient.cs b/Wino.Core.Domain/Interfaces/IShellClient.cs index 2248a92e..4d408f16 100644 --- a/Wino.Core.Domain/Interfaces/IShellClient.cs +++ b/Wino.Core.Domain/Interfaces/IShellClient.cs @@ -36,6 +36,7 @@ public interface IMailShellClient : IShellClient IMenuItem CreatePrimaryMenuItem { get; } IEnumerable GetFolderContextMenuActions(IBaseFolderMenuItem folder); + Task HandleAccountCreatedAsync(MailAccount createdAccount); Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem, TaskCompletionSource? folderInitAwaitTask = null); Task ChangeLoadedAccountAsync(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true); Task PerformFolderOperationAsync(FolderOperation operation, IBaseFolderMenuItem folderMenuItem); diff --git a/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs b/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs index fa10bc7e..4f8031d6 100644 --- a/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs +++ b/Wino.Core.Domain/Interfaces/IStatePersistenceService.cs @@ -45,9 +45,9 @@ public interface IStatePersistanceService : INotifyPropertyChanged bool IsEventDetailsVisible { get; set; } /// - /// Whether SettingsPage has navigated to a sub-page and can go back. + /// Whether the current application mode has an active backstack that can be navigated. /// - bool IsSettingsNavigating { get; set; } + bool HasCurrentModeBackStack { get; set; } /// /// Setting: Opened pane length for the navigation view. diff --git a/Wino.Core.Domain/Models/Calendar/AccountCalendarPickingResult.cs b/Wino.Core.Domain/Models/Calendar/AccountCalendarPickingResult.cs new file mode 100644 index 00000000..8cbe511a --- /dev/null +++ b/Wino.Core.Domain/Models/Calendar/AccountCalendarPickingResult.cs @@ -0,0 +1,7 @@ +#nullable enable + +using Wino.Core.Domain.Entities.Calendar; + +namespace Wino.Core.Domain.Models.Calendar; + +public sealed record AccountCalendarPickingResult(AccountCalendar? PickedCalendar, bool ShouldNavigateToCalendarSettings); diff --git a/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs b/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs index 91cb2188..ba890092 100644 --- a/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs +++ b/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Wino.Core.Domain.Enums; @@ -9,13 +10,15 @@ public sealed class SettingsNavigationItemInfo( string title, string description, string glyph = "", - bool isSeparator = false) + bool isSeparator = false, + string searchKeywords = "") { public WinoPage? PageType { get; } = pageType; public string Title { get; } = title; public string Description { get; } = description; public string Glyph { get; } = glyph; public bool IsSeparator { get; } = isSeparator; + public string SearchKeywords { get; } = searchKeywords; } public static class SettingsNavigationInfoProvider @@ -31,53 +34,90 @@ public static class SettingsNavigationInfoProvider new(WinoPage.ManageAccountsPage, Translator.SettingsManageAccountSettings_Title, manageAccountsDescription, - "\uE77B"), + "\uE77B", + searchKeywords: Translator.SettingsSearch_ManageAccounts_Keywords), new(null, Translator.SettingsOptions_GeneralSection, string.Empty, "\uE713", isSeparator: true), new(WinoPage.AppPreferencesPage, Translator.SettingsAppPreferences_Title, Translator.SettingsAppPreferences_Description, - "\uE770"), + "\uE770", + searchKeywords: Translator.SettingsSearch_AppPreferences_Keywords), new(WinoPage.LanguageTimePage, Translator.SettingsLanguageTime_Title, Translator.SettingsLanguageTime_Description, - "\uE775"), + "\uE775", + searchKeywords: Translator.SettingsSearch_LanguageTime_Keywords), new(WinoPage.PersonalizationPage, Translator.SettingsPersonalization_Title, Translator.SettingsPersonalization_Description, - "\uE771"), + "\uE771", + searchKeywords: Translator.SettingsSearch_Personalization_Keywords), new(WinoPage.AboutPage, Translator.SettingsAbout_Title, Translator.SettingsAbout_Description, - "\uE946"), + "\uE946", + searchKeywords: Translator.SettingsSearch_About_Keywords), new(null, Translator.SettingsOptions_MailSection, string.Empty, "\uE715", isSeparator: true), new(WinoPage.KeyboardShortcutsPage, Translator.Settings_KeyboardShortcuts_Title, Translator.Settings_KeyboardShortcuts_Description, - "\uE765"), + "\uE765", + searchKeywords: Translator.SettingsSearch_KeyboardShortcuts_Keywords), new(WinoPage.MessageListPage, Translator.SettingsMessageList_Title, Translator.SettingsMessageList_Description, - "\uE8C4"), + "\uE8C4", + searchKeywords: Translator.SettingsSearch_MessageList_Keywords), new(WinoPage.ReadComposePanePage, Translator.SettingsReadComposePane_Title, Translator.SettingsReadComposePane_Description, - "\uE8BD"), + "\uE8BD", + searchKeywords: Translator.SettingsSearch_ReadComposePane_Keywords), new(WinoPage.SignatureAndEncryptionPage, Translator.SettingsSignatureAndEncryption_Title, Translator.SettingsSignatureAndEncryption_Description, - "\uE8D7"), + "\uE8D7", + searchKeywords: Translator.SettingsSearch_SignatureAndEncryption_Keywords), new(WinoPage.StoragePage, Translator.SettingsStorage_Title, Translator.SettingsStorage_Description, - "\uE81C"), + "\uE81C", + searchKeywords: Translator.SettingsSearch_Storage_Keywords), new(null, Translator.SettingsOptions_CalendarSection, string.Empty, "\uE787", isSeparator: true), new(WinoPage.CalendarSettingsPage, Translator.SettingsCalendarSettings_Title, Translator.SettingsCalendarSettings_Description, - "\uE787") + "\uE787", + searchKeywords: Translator.SettingsSearch_CalendarSettings_Keywords) ]; } + public static IReadOnlyList Search(string query, string manageAccountsDescription = "") + { + if (string.IsNullOrWhiteSpace(query)) + return []; + + var normalizedQuery = NormalizeSearchText(query); + + if (string.IsNullOrWhiteSpace(normalizedQuery)) + return []; + + var queryTerms = normalizedQuery.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + return GetNavigationItems(manageAccountsDescription) + .Where(item => item.PageType.HasValue && !item.IsSeparator && item.PageType.Value != WinoPage.SettingOptionsPage) + .Select(item => new + { + Item = item, + Score = CalculateSearchScore(item, normalizedQuery, queryTerms) + }) + .Where(x => x.Score > 0) + .OrderByDescending(x => x.Score) + .ThenBy(x => x.Item.Title) + .Select(x => x.Item) + .ToList(); + } + public static SettingsNavigationItemInfo GetInfo(WinoPage pageType, string manageAccountsDescription = "") { var rootPage = GetRootPage(pageType); @@ -119,4 +159,58 @@ public static class SettingsNavigationInfoProvider WinoPage.CalendarAccountSettingsPage => WinoPage.CalendarSettingsPage, _ => pageType }; + + private static int CalculateSearchScore(SettingsNavigationItemInfo item, string normalizedQuery, IReadOnlyList queryTerms) + { + var title = NormalizeSearchText(item.Title); + var description = NormalizeSearchText(item.Description); + var keywords = NormalizeSearchText(item.SearchKeywords); + var combinedText = string.Join(' ', new[] { title, description, keywords }.Where(text => !string.IsNullOrWhiteSpace(text))); + + if (!combinedText.Contains(normalizedQuery, StringComparison.Ordinal) && + !queryTerms.All(term => combinedText.Contains(term, StringComparison.Ordinal))) + { + return 0; + } + + var score = 0; + + if (title.StartsWith(normalizedQuery, StringComparison.Ordinal)) + score += 500; + else if (title.Contains(normalizedQuery, StringComparison.Ordinal)) + score += 360; + + if (keywords.Contains(normalizedQuery, StringComparison.Ordinal)) + score += 280; + + if (description.Contains(normalizedQuery, StringComparison.Ordinal)) + score += 180; + + foreach (var term in queryTerms) + { + if (title.Contains(term, StringComparison.Ordinal)) + score += 70; + + if (keywords.Contains(term, StringComparison.Ordinal)) + score += 50; + + if (description.Contains(term, StringComparison.Ordinal)) + score += 30; + } + + return score; + } + + private static string NormalizeSearchText(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + var sanitized = value + .ToLowerInvariant() + .Select(character => char.IsLetterOrDigit(character) ? character : ' ') + .ToArray(); + + return string.Join(' ', new string(sanitized).Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } } diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 34e24fc1..2dd27a95 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -143,6 +143,8 @@ "CalendarEventCompose_Location": "Location", "CalendarEventCompose_LocationPlaceholder": "Add a location", "CalendarEventCompose_NewEventButton": "New Event", + "CalendarEventCompose_DefaultCalendarHint": "You can choose a default calendar for new events in Calendar settings.", + "CalendarEventCompose_DefaultCalendarSettingsLink": "Open Calendar settings", "CalendarEventCompose_NoCalendarsMessage": "There are no calendars available for event creation yet.", "CalendarEventCompose_NoCalendarsTitle": "No calendars available", "CalendarEventCompose_NoEndDate": "No end date", @@ -816,6 +818,22 @@ "SettingsNotificationsAndTaskbar_Description": "Change whether notifications should be displayed and taskbar badge for this account.", "SettingsNotificationsAndTaskbar_Title": "Notifications & Taskbar", "SettingsHome_Title": "Home", + "SettingsHome_SearchTitle": "Find a setting", + "SettingsHome_SearchDescription": "Search by feature, topic, or keyword to jump straight to the right settings page.", + "SettingsHome_SearchPlaceholder": "Search settings", + "SettingsHome_SearchExamples": "Try: theme, storage, language, signature", + "SettingsHome_QuickLinks_Title": "Quick links", + "SettingsHome_QuickLinks_Description": "Jump into the settings people reach for most often.", + "SettingsHome_StorageCard_Description": "See how much local MIME content Wino keeps on this device and clean it up when needed.", + "SettingsHome_StorageEmptySummary": "No cached MIME content detected yet.", + "SettingsHome_StorageLoading": "Checking local MIME usage...", + "SettingsHome_Tips_Title": "Tips & tricks", + "SettingsHome_Tips_Description": "A few small changes can make Wino feel much more personal.", + "SettingsHome_Tip_Theme": "Want dark mode or accent changes? Open Personalization.", + "SettingsHome_Tip_Background": "Use App Preferences to control startup behavior and background sync.", + "SettingsHome_Tip_Shortcuts": "Keyboard Shortcuts helps you move through mail faster.", + "SettingsHome_Resources_Title": "Helpful links", + "SettingsHome_Resources_Description": "Open project resources, support info, and release channels.", "SettingsOptions_Title": "Settings", "SettingsOptions_GeneralSection": "General", "SettingsOptions_MailSection": "Mail", @@ -823,6 +841,17 @@ "SettingsOptions_MoreComingSoon": "More options coming soon", "SettingsOptions_HeroDescription": "Customize your Wino Mail experience", "SettingsOptions_AccountsSummary": "{0} account(s) configured", + "SettingsSearch_ManageAccounts_Keywords": "account;accounts;mailbox;mailboxes;alias;aliases;profile;address;addresses", + "SettingsSearch_AppPreferences_Keywords": "startup;background;launch;sync;notification;notifications;search;tray;defaults", + "SettingsSearch_LanguageTime_Keywords": "language;time;clock;locale;region;format;24 hour;24h", + "SettingsSearch_Personalization_Keywords": "theme;dark;light;appearance;accent;color;colour;mode;layout;density", + "SettingsSearch_About_Keywords": "about;version;website;privacy;github;donate;store;support", + "SettingsSearch_KeyboardShortcuts_Keywords": "shortcut;shortcuts;hotkey;hotkeys;keyboard;keys", + "SettingsSearch_MessageList_Keywords": "message;messages;list;threading;threads;avatar;preview;sender", + "SettingsSearch_ReadComposePane_Keywords": "reader;compose;composer;font;fonts;external content;display;reading", + "SettingsSearch_SignatureAndEncryption_Keywords": "signature;signatures;encryption;certificate;certificates;s mime;smime;security", + "SettingsSearch_Storage_Keywords": "storage;cache;caching;mime;disk;space;cleanup;clean up;local data", + "SettingsSearch_CalendarSettings_Keywords": "calendar;week;hours;schedule;event;events", "SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.", "SettingsPaneLengthReset_Title": "Reset Mail List Size", "SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.", diff --git a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs index 4498e772..6abc8c6d 100644 --- a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs +++ b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs @@ -1,4 +1,7 @@ -using System; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -8,6 +11,7 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Settings; +using Wino.Core.Extensions; using Wino.Messaging.Client.Navigation; namespace Wino.Core.ViewModels; @@ -16,10 +20,15 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel { private readonly INativeAppService _nativeAppService; private readonly IAccountService _accountService; + private readonly IMimeStorageService _mimeStorageService; private readonly IStoreRatingService _storeRatingService; public string GitHubUrl => AppUrls.GitHub; public string PaypalUrl => AppUrls.Paypal; + public string WebsiteUrl => AppUrls.Website; + public string PrivacyPolicyUrl => AppUrls.PrivacyPolicy; + + public ObservableCollection SearchSuggestions { get; } = []; [ObservableProperty] public partial string VersionText { get; set; } = string.Empty; @@ -30,12 +39,20 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel [ObservableProperty] public partial int AccountCount { get; set; } + [ObservableProperty] + public partial string StorageSummaryText { get; set; } = string.Empty; + + [ObservableProperty] + public partial string SearchQuery { get; set; } = string.Empty; + public SettingOptionsPageViewModel(INativeAppService nativeAppService, IAccountService accountService, + IMimeStorageService mimeStorageService, IStoreRatingService storeRatingService) { _nativeAppService = nativeAppService; _accountService = accountService; + _mimeStorageService = mimeStorageService; _storeRatingService = storeRatingService; } @@ -44,18 +61,57 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel base.OnNavigatedTo(mode, parameters); VersionText = string.Format("{0}{1}", Translator.SettingsAboutVersion, _nativeAppService.GetFullAppVersion()); - _ = LoadAccountSummaryAsync(); + SearchQuery = string.Empty; + SearchSuggestions.Clear(); + StorageSummaryText = Translator.SettingsHome_StorageLoading; + + _ = LoadDashboardAsync(); } - private async Task LoadAccountSummaryAsync() + public void UpdateSearchSuggestions(string query) { - var accounts = await _accountService.GetAccountsAsync(); - int count = accounts?.Count ?? 0; + SearchQuery = query; + + SearchSuggestions.Clear(); + + foreach (var result in SettingsNavigationInfoProvider.Search(query, AccountSummaryText).Take(6)) + { + SearchSuggestions.Add(result); + } + } + + public SettingsNavigationItemInfo GetBestSearchSuggestion(string query) + => SettingsNavigationInfoProvider.Search(query, AccountSummaryText).FirstOrDefault(); + + public void NavigateToSetting(SettingsNavigationItemInfo item) + { + if (item?.PageType is WinoPage pageType) + { + NavigateSubDetail(pageType); + } + } + + private async Task LoadDashboardAsync() + { + var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false) ?? []; + var count = accounts.Count; + Dictionary storageSizeMap = count == 0 + ? [] + : await _mimeStorageService.GetAccountsMimeStorageSizesAsync(accounts.Select(account => account.Id)).ConfigureAwait(false); + var totalStorageBytes = storageSizeMap.Values.Sum(); await ExecuteUIThread(() => { AccountCount = count; AccountSummaryText = string.Format(Translator.SettingsOptions_AccountsSummary, count); + StorageSummaryText = totalStorageBytes == 0 + ? Translator.SettingsHome_StorageEmptySummary + : string.Format(Translator.SettingsStorage_TotalUsage, totalStorageBytes.GetBytesReadable()); + + if (!string.IsNullOrWhiteSpace(SearchQuery)) + { + UpdateSearchSuggestions(SearchQuery); + } }); } diff --git a/Wino.Mail.ViewModels/MailAppShellViewModel.cs b/Wino.Mail.ViewModels/MailAppShellViewModel.cs index 82e32754..d2b15651 100644 --- a/Wino.Mail.ViewModels/MailAppShellViewModel.cs +++ b/Wino.Mail.ViewModels/MailAppShellViewModel.cs @@ -40,8 +40,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel, IRecipient, IRecipient, IRecipient, - IRecipient, - IRecipient + IRecipient { #region Menu Items @@ -1154,7 +1153,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel, { base.RegisterRecipients(); - Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); @@ -1172,7 +1170,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel, { base.UnregisterRecipients(); - Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); @@ -1198,14 +1195,17 @@ public partial class MailAppShellViewModel : MailBaseViewModel, await RestoreSelectedAccountAfterMenuRefreshAsync(false); } - public async void Receive(AccountCreatedMessage message) + public async Task HandleAccountCreatedAsync(MailAccount createdAccount) { - var createdAccount = message.Account; latestSelectedAccountMenuItem = null; await RecreateMenuItemsAsync(); - if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) return; + if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) + { + Log.Warning("Created account {AccountId} could not be found in menu items after refresh.", createdAccount.Id); + return; + } await ChangeLoadedAccountAsync(createdMenuItem); diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index d683c653..5ae22e89 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -722,6 +722,7 @@ public partial class App : WinoApplication, public void Receive(AccountCreatedMessage message) { var windowManager = Services.GetRequiredService(); + var navigationService = Services.GetRequiredService(); // Only transition when the account was created from the WelcomeWindow. if (windowManager.GetWindow(WinoWindowKind.Welcome) == null) @@ -732,6 +733,7 @@ public partial class App : WinoApplication, // Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher. CreateWindow(null); windowManager.HideWindow(WinoWindowKind.Welcome); + navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail); await NewThemeService.ApplyThemeToActiveWindowAsync(); MainWindow?.Activate(); RestartAutoSynchronizationLoop(); diff --git a/Wino.Mail.WinUI/Dialogs/SingleCalendarPickerDialog.xaml b/Wino.Mail.WinUI/Dialogs/SingleCalendarPickerDialog.xaml index 62f2d4b8..08af00eb 100644 --- a/Wino.Mail.WinUI/Dialogs/SingleCalendarPickerDialog.xaml +++ b/Wino.Mail.WinUI/Dialogs/SingleCalendarPickerDialog.xaml @@ -25,49 +25,62 @@ - - - - - - - - - - - + + - - - - - - - - + - + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Dialogs/SingleCalendarPickerDialog.xaml.cs b/Wino.Mail.WinUI/Dialogs/SingleCalendarPickerDialog.xaml.cs index bf4b5641..97eb120c 100644 --- a/Wino.Mail.WinUI/Dialogs/SingleCalendarPickerDialog.xaml.cs +++ b/Wino.Mail.WinUI/Dialogs/SingleCalendarPickerDialog.xaml.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Models.Calendar; @@ -8,6 +9,7 @@ namespace Wino.Dialogs; public sealed partial class SingleCalendarPickerDialog : ContentDialog { public AccountCalendar? PickedCalendar { get; private set; } + public bool ShouldNavigateToCalendarSettings { get; private set; } public List AvailableGroups { get; } = []; @@ -23,4 +25,10 @@ public sealed partial class SingleCalendarPickerDialog : ContentDialog PickedCalendar = e.ClickedItem as AccountCalendar; Hide(); } + + private void OpenCalendarSettingsClicked(object sender, RoutedEventArgs e) + { + ShouldNavigateToCalendarSettings = true; + Hide(); + } } diff --git a/Wino.Mail.WinUI/Services/DialogService.cs b/Wino.Mail.WinUI/Services/DialogService.cs index 793354d1..f9a3faa9 100644 --- a/Wino.Mail.WinUI/Services/DialogService.cs +++ b/Wino.Mail.WinUI/Services/DialogService.cs @@ -124,7 +124,7 @@ public class DialogService : DialogServiceBase, IMailDialogService return accountPicker.PickedAccount ?? null!; } - public async Task ShowSingleCalendarPickerDialogAsync(List availableCalendarGroups) + public async Task ShowSingleCalendarPickerDialogAsync(List availableCalendarGroups) { var calendarPicker = new SingleCalendarPickerDialog(availableCalendarGroups) { @@ -133,7 +133,7 @@ public class DialogService : DialogServiceBase, IMailDialogService await HandleDialogPresentationAsync(calendarPicker); - return calendarPicker.PickedCalendar ?? null!; + return new AccountCalendarPickingResult(calendarPicker.PickedCalendar, calendarPicker.ShouldNavigateToCalendarSettings); } public async Task ShowSignatureEditorDialog(AccountSignature? signatureModel = null) diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index e3bde408..7995e4ec 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -247,6 +247,8 @@ public class NavigationService : NavigationServiceBase, INavigationService IsInitialActivation = isInitialShellNavigation, Parameter = activationParameter }); + + ResetCurrentModeBackStackState(); return true; } @@ -323,6 +325,7 @@ public class NavigationService : NavigationServiceBase, INavigationService if (innerShellFrame.CanGoBack && lastBackStackEntry?.SourcePageType == pageType) { innerShellFrame.GoBack(); + UpdateCurrentModeBackStackState(innerShellFrame); WeakReferenceMessenger.Default.Send(loadCalendarMessage); return true; } @@ -483,7 +486,14 @@ public class NavigationService : NavigationServiceBase, INavigationService private bool NavigateInnerShellFrame(Frame frame, Type pageType, object? parameter, NavigationTransitionType transition) { var transitionInfo = ConsumeInnerShellTransitionOrDefault(transition); - return frame.Navigate(pageType, parameter, transitionInfo); + var navigationResult = frame.Navigate(pageType, parameter, transitionInfo); + + if (navigationResult) + { + UpdateCurrentModeBackStackState(frame); + } + + return navigationResult; } private NavigationTransitionInfo ConsumeInnerShellTransitionOrDefault(NavigationTransitionType transition) @@ -503,36 +513,67 @@ public class NavigationService : NavigationServiceBase, INavigationService private void GoBackInternal(Core.Domain.Enums.NavigationTransitionEffect slideEffect = Core.Domain.Enums.NavigationTransitionEffect.FromRight) { - if (_statePersistanceService.IsSettingsNavigating) + var innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame); + + if (_statePersistanceService.ApplicationMode == WinoApplicationMode.Settings && + _statePersistanceService.HasCurrentModeBackStack) { WeakReferenceMessenger.Default.Send(new BackBreadcrumNavigationRequested(slideEffect)); return; } - var innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame); + if (_statePersistanceService.ApplicationMode == WinoApplicationMode.Settings && + innerShellFrame?.Content is SettingsPage) + { + return; + } if (_statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar && innerShellFrame?.CanGoBack == true) { innerShellFrame.GoBack(); + UpdateCurrentModeBackStackState(innerShellFrame); // Calendar mode: Navigate back from EventDetailsPage _statePersistanceService.IsEventDetailsVisible = false; } - else + else if (_statePersistanceService.ApplicationMode == WinoApplicationMode.Mail) { if (_statePersistanceService.IsReadingMail && _statePersistanceService.IsReaderNarrowed) { // Mail mode: Clear selections and dispose rendering frame _statePersistanceService.IsReadingMail = false; + _statePersistanceService.HasCurrentModeBackStack = false; WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested()); WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested()); } - else if (innerShellFrame != null && innerShellFrame.CanGoBack) - { - innerShellFrame.GoBack(); - } } + else + { + UpdateCurrentModeBackStackState(innerShellFrame); + } + } + + private void ResetCurrentModeBackStackState() + { + var innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame); + + if (innerShellFrame != null) + { + innerShellFrame.BackStack.Clear(); + innerShellFrame.ForwardStack.Clear(); + } + + _statePersistanceService.HasCurrentModeBackStack = false; + } + + private void UpdateCurrentModeBackStackState(Frame? innerShellFrame) + { + if (_statePersistanceService.ApplicationMode == WinoApplicationMode.Settings) + return; + + _statePersistanceService.HasCurrentModeBackStack = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar && + innerShellFrame?.CanGoBack == true; } // Standalone EML viewer. diff --git a/Wino.Mail.WinUI/Services/StatePersistenceService.cs b/Wino.Mail.WinUI/Services/StatePersistenceService.cs index 67db9961..5a3088d6 100644 --- a/Wino.Mail.WinUI/Services/StatePersistenceService.cs +++ b/Wino.Mail.WinUI/Services/StatePersistenceService.cs @@ -34,8 +34,8 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic public bool IsBackButtonVisible => ApplicationMode == WinoApplicationMode.Mail - ? (IsReadingMail && IsReaderNarrowed) || IsSettingsNavigating - : IsEventDetailsVisible || IsSettingsNavigating; + ? (IsReadingMail && IsReaderNarrowed) || HasCurrentModeBackStack + : IsEventDetailsVisible || HasCurrentModeBackStack; private WinoApplicationMode applicationMode = WinoApplicationMode.Mail; @@ -68,14 +68,14 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic } } - private bool isSettingsNavigating; + private bool hasCurrentModeBackStack; - public bool IsSettingsNavigating + public bool HasCurrentModeBackStack { - get => isSettingsNavigating; + get => hasCurrentModeBackStack; set { - if (SetProperty(ref isSettingsNavigating, value)) + if (SetProperty(ref hasCurrentModeBackStack, value)) { OnPropertyChanged(nameof(IsBackButtonVisible)); } diff --git a/Wino.Mail.WinUI/Styles/DataTemplates.xaml b/Wino.Mail.WinUI/Styles/DataTemplates.xaml index d4b22582..59911da1 100644 --- a/Wino.Mail.WinUI/Styles/DataTemplates.xaml +++ b/Wino.Mail.WinUI/Styles/DataTemplates.xaml @@ -64,23 +64,51 @@ - - + + + + + + + + + + + - - + Style="{StaticResource CaptionTextBlockStyle}" + Text="{x:Bind Title}" + TextWrapping="NoWrap" /> + + + diff --git a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml index ca110a67..c7254768 100644 --- a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml +++ b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml @@ -3,43 +3,85 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:abstract="using:Wino.Views.Abstract" - xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:domain="using:Wino.Core.Domain" xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:settingsModels="using:Wino.Core.Domain.Models.Settings" x:Name="root" Title="{x:Bind domain:Translator.SettingsHome_Title}" mc:Ignorable="d"> - + - - - - - + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + - - + - - - - + - + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml.cs b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml.cs index 2ec1c820..9171c91e 100644 --- a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml.cs @@ -1,7 +1,7 @@ -using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Settings; using Wino.Views.Abstract; namespace Wino.Views.Settings; @@ -18,7 +18,6 @@ public sealed partial class SettingOptionsPage : SettingOptionsPageAbstract WinoPage? page = sender switch { Button button when button.Tag is WinoPage p => p, - SettingsCard card when card.Tag is WinoPage p => p, _ => null }; @@ -27,4 +26,28 @@ public sealed partial class SettingOptionsPage : SettingOptionsPageAbstract ViewModel.NavigateSubDetailCommand.Execute(page.Value); } } + + private void SettingsSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput || string.IsNullOrWhiteSpace(sender.Text)) + { + ViewModel.UpdateSearchSuggestions(sender.Text); + } + } + + private void SettingsSearchSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) + { + if (args.SelectedItem is SettingsNavigationItemInfo selectedSetting) + { + ViewModel.SearchQuery = selectedSetting.Title; + } + } + + private void SettingsSearchQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + var selectedSetting = args.ChosenSuggestion as SettingsNavigationItemInfo + ?? ViewModel.GetBestSearchSuggestion(args.QueryText); + + ViewModel.NavigateToSetting(selectedSetting); + } } diff --git a/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs b/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs index fa5fa562..e43a2b32 100644 --- a/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs @@ -71,7 +71,7 @@ public sealed partial class SettingsPage : SettingsPageAbstract, SettingsFrame.Navigated -= SettingsFrameNavigated; // Reset navigation state when leaving SettingsPage - ViewModel.StatePersistenceService.IsSettingsNavigating = false; + ViewModel.StatePersistenceService.HasCurrentModeBackStack = false; base.OnNavigatingFrom(e); } @@ -202,9 +202,23 @@ public sealed partial class SettingsPage : SettingsPageAbstract, UpdateWindowTitle(); } + public void ResetForModeSwitch() + { + while (PageHistory.Count > 1 && SettingsFrame.CanGoBack) + { + if (!BreadcrumbNavigationHelper.GoBack(SettingsFrame, PageHistory, Core.Domain.Enums.NavigationTransitionEffect.FromRight)) + break; + } + + SettingsFrame.ForwardStack.Clear(); + UpdateBackNavigationState(); + _ = RefreshCurrentPageStateAsync(); + UpdateWindowTitle(); + } + private void UpdateBackNavigationState() { - ViewModel.StatePersistenceService.IsSettingsNavigating = PageHistory.Count > 1 && SettingsFrame.CanGoBack; + ViewModel.StatePersistenceService.HasCurrentModeBackStack = PageHistory.Count > 1 && SettingsFrame.CanGoBack; } private async Task RefreshCurrentPageStateAsync() diff --git a/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs b/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs index dc174244..ac2bb687 100644 --- a/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs +++ b/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs @@ -33,6 +33,7 @@ using Wino.Messaging.Client.Accounts; using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Shell; +using Wino.Messaging.UI; using Wino.Views.Mail; using Wino.Views; using Wino.Views.Settings; @@ -45,7 +46,8 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient { private const string StateHorizontalCalendar = "HorizontalCalendar"; private const string StateVerticalCalendar = "VerticalCalendar"; @@ -97,6 +99,7 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, UpdateTitleBarSubtitle(); ViewModel.CurrentClient.Activate(activationContext); + ResetShellModeNavigationState(); ApplyTitleBarContent(); } @@ -158,6 +161,11 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, } else if (_activeMode == WinoApplicationMode.Settings) { + if (InnerShellFrame.Content is SettingsPage settingsPage) + { + settingsPage.ResetForModeSwitch(); + } + ViewModel.CurrentClient.Deactivate(); } @@ -166,7 +174,7 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, private void ResetShellModeNavigationState() { - ViewModel.StatePersistenceService.IsSettingsNavigating = false; + ViewModel.StatePersistenceService.HasCurrentModeBackStack = false; InnerShellFrame.BackStack.Clear(); InnerShellFrame.ForwardStack.Clear(); } @@ -284,6 +292,15 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, public void Receive(CalendarDisplayTypeChangedMessage message) => ManageCalendarDisplayType(message.NewDisplayType); + public void Receive(AccountCreatedMessage message) + { + _ = DispatcherQueue.EnqueueAsync(async () => + { + ViewModel.NavigationService.ChangeApplicationMode(WinoApplicationMode.Mail); + await ViewModel.MailClient.HandleAccountCreatedAsync(message.Account); + }); + } + private async void NavigationViewItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) { if (_isSyncingNavigationViewSelection) @@ -758,6 +775,7 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); } protected override void UnregisterRecipients() @@ -768,5 +786,6 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); } }