diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
index 6cafeb36..8930f74b 100644
--- a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
+++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
@@ -17,6 +17,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.ViewModels;
@@ -393,6 +394,18 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
});
}
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Calendar)
+ return;
+
+ if (args.Action == KeyboardShortcutAction.NewEvent)
+ {
+ await NewEventAsync();
+ args.Handled = true;
+ }
+ }
+
[RelayCommand]
diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
index 28f69fbb..5b9d7c59 100644
--- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
+++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
@@ -14,6 +14,7 @@ using Serilog;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Calendar.ViewModels.Messages;
+using Wino.Core.Domain;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
@@ -21,6 +22,7 @@ using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
@@ -135,6 +137,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
+ private readonly IMailDialogService _dialogService;
// Store latest rendered options.
private CalendarDisplayType _currentDisplayType;
@@ -156,7 +159,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
INativeAppService nativeAppService,
IAccountCalendarStateService accountCalendarStateService,
IPreferencesService preferencesService,
- IWinoRequestDelegator winoRequestDelegator)
+ IWinoRequestDelegator winoRequestDelegator,
+ IMailDialogService dialogService)
{
StatePersistanceService = statePersistanceService;
AccountCalendarStateService = accountCalendarStateService;
@@ -167,6 +171,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
_nativeAppService = nativeAppService;
_preferencesService = preferencesService;
_winoRequestDelegator = winoRequestDelegator;
+ _dialogService = dialogService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
@@ -175,6 +180,35 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
RegisterRecipients();
}
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete)
+ return;
+
+ if (DisplayDetailsCalendarItemViewModel?.CalendarItem == null)
+ return;
+
+ if (DisplayDetailsCalendarItemViewModel.CalendarItem.IsRecurringParent)
+ {
+ var confirmed = await _dialogService.ShowConfirmationDialogAsync(
+ Translator.DialogMessage_DeleteRecurringSeriesMessage,
+ Translator.DialogMessage_DeleteRecurringSeriesTitle,
+ Translator.Buttons_Delete);
+
+ if (!confirmed)
+ return;
+ }
+
+ var preparationRequest = new CalendarOperationPreparationRequest(
+ CalendarSynchronizerOperation.DeleteEvent,
+ DisplayDetailsCalendarItemViewModel.CalendarItem,
+ null);
+
+ await _winoRequestDelegator.ExecuteAsync(preparationRequest);
+ DisplayDetailsCalendarItemViewModel = null;
+ args.Handled = true;
+ }
+
protected override void RegisterRecipients()
{
base.RegisterRecipients();
diff --git a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs
index f2064610..5840a669 100644
--- a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs
+++ b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs
@@ -15,6 +15,7 @@ using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Services;
using Wino.Core.ViewModels;
@@ -498,6 +499,15 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
}
}
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete)
+ return;
+
+ await DeleteAsync();
+ args.Handled = true;
+ }
+
[RelayCommand]
private Task JoinOnlineAsync()
{
diff --git a/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs b/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs
index d89c4952..8b066ad5 100644
--- a/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs
+++ b/Wino.Core.Domain/Entities/Shared/KeyboardShortcut.cs
@@ -12,6 +12,11 @@ public class KeyboardShortcut
[PrimaryKey]
public Guid Id { get; set; }
+ ///
+ /// The application mode this shortcut applies to.
+ ///
+ public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail;
+
///
/// The key combination string (e.g., "D", "Delete", "F1").
///
@@ -23,9 +28,9 @@ public class KeyboardShortcut
public ModifierKeys ModifierKeys { get; set; }
///
- /// The mail operation this shortcut triggers.
+ /// The shortcut action this shortcut triggers.
///
- public MailOperation MailOperation { get; set; }
+ public KeyboardShortcutAction Action { get; set; }
///
/// Whether this shortcut is enabled.
@@ -55,6 +60,6 @@ public class KeyboardShortcut
modifierText += "Win+";
return modifierText + Key;
- }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs b/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs
new file mode 100644
index 00000000..f48f5cf4
--- /dev/null
+++ b/Wino.Core.Domain/Enums/KeyboardShortcutAction.cs
@@ -0,0 +1,16 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum KeyboardShortcutAction
+{
+ None,
+ NewMail,
+ ToggleReadUnread,
+ ToggleFlag,
+ ToggleArchive,
+ Delete,
+ Move,
+ Reply,
+ ReplyAll,
+ Send,
+ NewEvent
+}
diff --git a/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs b/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs
index 1e7098ec..eb242d84 100644
--- a/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs
+++ b/Wino.Core.Domain/Interfaces/IKeyboardShortcutService.cs
@@ -37,21 +37,23 @@ public interface IKeyboardShortcutService
Task DeleteKeyboardShortcutAsync(Guid shortcutId);
///
- /// Gets the mail operation for the given key combination.
+ /// Gets the keyboard shortcut for the given key combination in a specific mode.
///
+ /// The application mode to search within.
/// The pressed key.
/// The modifier keys pressed.
- /// The mail operation if found, otherwise null.
- Task GetMailOperationForKeyAsync(string key, ModifierKeys modifierKeys);
+ /// The matching shortcut if found, otherwise null.
+ Task GetShortcutForKeyAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys);
///
/// Checks if a key combination is already assigned to another shortcut.
///
+ /// The application mode to check within.
/// The key to check.
/// The modifier keys to check.
/// Optional ID to exclude from the check (for updates).
/// True if the combination is already used, false otherwise.
- Task IsKeyCombinationInUseAsync(string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null);
+ Task IsKeyCombinationInUseAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null);
///
/// Creates default keyboard shortcuts for common mail operations.
@@ -62,4 +64,4 @@ public interface IKeyboardShortcutService
/// Resets all shortcuts to defaults.
///
Task ResetToDefaultShortcutsAsync();
-}
\ No newline at end of file
+}
diff --git a/Wino.Core.Domain/Models/KeyboardShortcutDialogResult.cs b/Wino.Core.Domain/Models/KeyboardShortcutDialogResult.cs
index 4c4cab97..d8079ce5 100644
--- a/Wino.Core.Domain/Models/KeyboardShortcutDialogResult.cs
+++ b/Wino.Core.Domain/Models/KeyboardShortcutDialogResult.cs
@@ -12,6 +12,11 @@ public class KeyboardShortcutDialogResult
///
public bool IsSuccess { get; set; }
+ ///
+ /// The application mode selected by the user.
+ ///
+ public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail;
+
///
/// The key combination entered by the user.
///
@@ -23,21 +28,22 @@ public class KeyboardShortcutDialogResult
public ModifierKeys ModifierKeys { get; set; }
///
- /// The mail operation selected by the user.
+ /// The shortcut action selected by the user.
///
- public MailOperation MailOperation { get; set; }
+ public KeyboardShortcutAction Action { get; set; }
///
/// Creates a successful result.
///
- public static KeyboardShortcutDialogResult Success(string key, ModifierKeys modifierKeys, MailOperation mailOperation)
+ public static KeyboardShortcutDialogResult Success(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, KeyboardShortcutAction action)
{
return new KeyboardShortcutDialogResult
{
IsSuccess = true,
+ Mode = mode,
Key = key,
ModifierKeys = modifierKeys,
- MailOperation = mailOperation
+ Action = action
};
}
@@ -51,4 +57,4 @@ public class KeyboardShortcutDialogResult
IsSuccess = false
};
}
-}
\ No newline at end of file
+}
diff --git a/Wino.Core.Domain/Models/KeyboardShortcutTriggerDetails.cs b/Wino.Core.Domain/Models/KeyboardShortcutTriggerDetails.cs
new file mode 100644
index 00000000..40ac92b2
--- /dev/null
+++ b/Wino.Core.Domain/Models/KeyboardShortcutTriggerDetails.cs
@@ -0,0 +1,16 @@
+using System;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Core.Domain.Models;
+
+public class KeyboardShortcutTriggerDetails
+{
+ public Guid ShortcutId { get; init; }
+ public WinoApplicationMode Mode { get; init; }
+ public KeyboardShortcutAction Action { get; init; }
+ public string Key { get; init; } = string.Empty;
+ public ModifierKeys ModifierKeys { get; init; }
+ public bool Handled { get; set; }
+ public object Sender { get; init; }
+ public object Origin { get; init; }
+}
diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json
index 58821952..51b0f6d5 100644
--- a/Wino.Core.Domain/Translations/en_US/resources.json
+++ b/Wino.Core.Domain/Translations/en_US/resources.json
@@ -362,19 +362,29 @@
"KeyboardShortcuts_FailedToReset": "Failed to reset keyboard shortcuts.",
"KeyboardShortcuts_FailedToUpdate": "Failed to update keyboard shortcuts",
"KeyboardShortcuts_MailoperationAction": "Action",
+ "KeyboardShortcuts_Action": "Action",
"KeyboardShortcuts_FailedToLoad": "Failed to load keyboard shortcuts.",
"KeyboardShortcuts_EnterKeyForShortcut": "Please enter a key for the shortcut.",
"KeyboardShortcuts_SelectOperationForShortcut": "Please an action to perform for the shortcut.",
+ "KeyboardShortcuts_EnterKey": "Please enter a key for the shortcut.",
+ "KeyboardShortcuts_SelectOperation": "Please select an action for the shortcut.",
"KeyboardShortcuts_ShortcutInUse": "This shortcut is already in use by another s hortcut.",
"KeyboardShortcuts_FailedToSave": "Failed to save the shortcut.",
"KeyboardShortcuts_FailedToDelete": "Failed to delete the shortcut.",
"KeyboardShortcuts_PageDescription": "Set up keyboard shortcuts for quick mail operations. Press keys while focused on the key input field to capture shortcuts.",
"KeyboardShortcuts_Add": "Add shortcut",
+ "KeyboardShortcuts_EditTitle": "Edit Keyboard Shortcut",
"KeyboardShortcuts_ResetToDefaults": "Reset to Defaults",
"KeyboardShortcuts_PressKeysHere": "Press keys here...",
"KeyboardShortcuts_KeyCombination": "Key Combination",
"KeyboardShortcuts_FocusArea": "Focus the field above and press the desired key combination",
"KeyboardShortcuts_Modifiers": "Modifier Keys",
+ "KeyboardShortcuts_Mode": "App Mode",
+ "KeyboardShortcuts_ModeMail": "Mail",
+ "KeyboardShortcuts_ModeCalendar": "Calendar",
+ "KeyboardShortcuts_ActionToggleReadUnread": "Toggle read/unread",
+ "KeyboardShortcuts_ActionToggleFlag": "Toggle flag",
+ "KeyboardShortcuts_ActionToggleArchive": "Toggle archive/unarchive",
"ImageRenderingDisabled": "Image rendering is disabled for this message.",
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
diff --git a/Wino.Core.ViewModels/CoreBaseViewModel.cs b/Wino.Core.ViewModels/CoreBaseViewModel.cs
index bf604998..17abbf45 100644
--- a/Wino.Core.ViewModels/CoreBaseViewModel.cs
+++ b/Wino.Core.ViewModels/CoreBaseViewModel.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation;
namespace Wino.Core.ViewModels;
@@ -40,6 +41,8 @@ public class CoreBaseViewModel : ObservableRecipient, INavigationAware
public virtual void OnPageLoaded() { }
+ public virtual Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) => Task.CompletedTask;
+
public Task ExecuteUIThread(Action action)
{
if (action == null) return Task.CompletedTask;
diff --git a/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs b/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
index 55941039..4d39385b 100644
--- a/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
+++ b/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
@@ -13,13 +13,16 @@ public partial class BreadcrumbNavigationItemViewModel : ObservableObject
public int StepNumber { get; set; }
+ public int BackStackDepth { get; set; }
+
public BreadcrumbNavigationRequested Request { get; set; }
- public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive, int stepNumber = 0)
+ public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive, int stepNumber = 0, int backStackDepth = 0)
{
Request = request;
Title = request.PageTitle;
IsActive = isActive;
StepNumber = stepNumber;
+ BackStackDepth = backStackDepth;
}
}
diff --git a/Wino.Core.ViewModels/Data/KeyboardShortcutActionViewModel.cs b/Wino.Core.ViewModels/Data/KeyboardShortcutActionViewModel.cs
new file mode 100644
index 00000000..85a2c624
--- /dev/null
+++ b/Wino.Core.ViewModels/Data/KeyboardShortcutActionViewModel.cs
@@ -0,0 +1,33 @@
+using Wino.Core.Domain;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Core.ViewModels.Data;
+
+public class KeyboardShortcutActionViewModel
+{
+ public WinoApplicationMode Mode { get; }
+ public KeyboardShortcutAction Action { get; }
+
+ public string DisplayName => Action switch
+ {
+ KeyboardShortcutAction.NewMail => Translator.MenuNewMail,
+ KeyboardShortcutAction.ToggleReadUnread => Translator.KeyboardShortcuts_ActionToggleReadUnread,
+ KeyboardShortcutAction.ToggleFlag => Translator.KeyboardShortcuts_ActionToggleFlag,
+ KeyboardShortcutAction.ToggleArchive => Translator.KeyboardShortcuts_ActionToggleArchive,
+ KeyboardShortcutAction.Delete => Translator.Buttons_Delete,
+ KeyboardShortcutAction.Move => Translator.MailOperation_Move,
+ KeyboardShortcutAction.Reply => Translator.MailOperation_Reply,
+ KeyboardShortcutAction.ReplyAll => Translator.MailOperation_ReplyAll,
+ KeyboardShortcutAction.Send => Translator.Buttons_Send,
+ KeyboardShortcutAction.NewEvent => Translator.CalendarEventCompose_NewEventButton,
+ _ => Action.ToString()
+ };
+
+ public KeyboardShortcutActionViewModel(WinoApplicationMode mode, KeyboardShortcutAction action)
+ {
+ Mode = mode;
+ Action = action;
+ }
+
+ public override string ToString() => DisplayName;
+}
diff --git a/Wino.Core.ViewModels/Data/KeyboardShortcutViewModel.cs b/Wino.Core.ViewModels/Data/KeyboardShortcutViewModel.cs
index 36c7b190..6ee4e1cb 100644
--- a/Wino.Core.ViewModels/Data/KeyboardShortcutViewModel.cs
+++ b/Wino.Core.ViewModels/Data/KeyboardShortcutViewModel.cs
@@ -15,9 +15,10 @@ public partial class KeyboardShortcutViewModel : ObservableObject
public partial bool IsEnabled { get; set; }
public Guid Id { get; }
+ public WinoApplicationMode Mode { get; }
public string Key { get; }
public ModifierKeys ModifierKeys { get; }
- public MailOperation MailOperation { get; }
+ public KeyboardShortcutAction Action { get; }
public DateTime CreatedAt { get; }
public string DisplayName
@@ -38,25 +39,30 @@ public partial class KeyboardShortcutViewModel : ObservableObject
}
}
- public string MailOperationDisplayName
+ public string ModeDisplayName => Mode switch
+ {
+ WinoApplicationMode.Mail => Translator.KeyboardShortcuts_ModeMail,
+ WinoApplicationMode.Calendar => Translator.KeyboardShortcuts_ModeCalendar,
+ _ => Mode.ToString()
+ };
+
+ public string ActionDisplayName
{
get
{
- return MailOperation switch
+ return Action switch
{
- MailOperation.Archive => Translator.MailOperation_Archive,
- MailOperation.UnArchive => Translator.MailOperation_Unarchive,
- MailOperation.SoftDelete => Translator.MailOperation_Delete,
- MailOperation.Move => Translator.MailOperation_Move,
- MailOperation.MoveToJunk => Translator.MailOperation_MoveJunk,
- MailOperation.SetFlag => Translator.MailOperation_SetFlag,
- MailOperation.ClearFlag => Translator.MailOperation_ClearFlag,
- MailOperation.MarkAsRead => Translator.MailOperation_MarkAsRead,
- MailOperation.MarkAsUnread => Translator.MailOperation_MarkAsUnread,
- MailOperation.Reply => Translator.MailOperation_Reply,
- MailOperation.ReplyAll => Translator.MailOperation_ReplyAll,
- MailOperation.Forward => Translator.MailOperation_Forward,
- _ => MailOperation.ToString()
+ KeyboardShortcutAction.NewMail => Translator.MenuNewMail,
+ KeyboardShortcutAction.ToggleReadUnread => Translator.KeyboardShortcuts_ActionToggleReadUnread,
+ KeyboardShortcutAction.ToggleFlag => Translator.KeyboardShortcuts_ActionToggleFlag,
+ KeyboardShortcutAction.ToggleArchive => Translator.KeyboardShortcuts_ActionToggleArchive,
+ KeyboardShortcutAction.Delete => Translator.Buttons_Delete,
+ KeyboardShortcutAction.Move => Translator.MailOperation_Move,
+ KeyboardShortcutAction.Reply => Translator.MailOperation_Reply,
+ KeyboardShortcutAction.ReplyAll => Translator.MailOperation_ReplyAll,
+ KeyboardShortcutAction.Send => Translator.Buttons_Send,
+ KeyboardShortcutAction.NewEvent => Translator.CalendarEventCompose_NewEventButton,
+ _ => Action.ToString()
};
}
}
@@ -64,9 +70,10 @@ public partial class KeyboardShortcutViewModel : ObservableObject
public KeyboardShortcutViewModel(KeyboardShortcut shortcut)
{
Id = shortcut.Id;
+ Mode = shortcut.Mode;
Key = shortcut.Key;
ModifierKeys = shortcut.ModifierKeys;
- MailOperation = shortcut.MailOperation;
+ Action = shortcut.Action;
CreatedAt = shortcut.CreatedAt;
IsEnabled = shortcut.IsEnabled;
}
@@ -76,9 +83,10 @@ public partial class KeyboardShortcutViewModel : ObservableObject
return new KeyboardShortcut
{
Id = Id,
+ Mode = Mode,
Key = Key,
ModifierKeys = ModifierKeys,
- MailOperation = MailOperation,
+ Action = Action,
CreatedAt = CreatedAt,
IsEnabled = IsEnabled
};
diff --git a/Wino.Core.ViewModels/KeyboardShortcutsPageViewModel.cs b/Wino.Core.ViewModels/KeyboardShortcutsPageViewModel.cs
index e190c0c5..a4c27da2 100644
--- a/Wino.Core.ViewModels/KeyboardShortcutsPageViewModel.cs
+++ b/Wino.Core.ViewModels/KeyboardShortcutsPageViewModel.cs
@@ -70,7 +70,7 @@ public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
try
{
// Check if key combination is already in use
- var isInUse = await _keyboardShortcutService.IsKeyCombinationInUseAsync(result.Key, result.ModifierKeys, null);
+ var isInUse = await _keyboardShortcutService.IsKeyCombinationInUseAsync(result.Mode, result.Key, result.ModifierKeys, null);
if (isInUse)
{
await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
@@ -80,9 +80,10 @@ public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
// Create new shortcut
var shortcut = new KeyboardShortcut
{
+ Mode = result.Mode,
Key = result.Key,
ModifierKeys = result.ModifierKeys,
- MailOperation = result.MailOperation,
+ Action = result.Action,
IsEnabled = true
};
@@ -116,7 +117,7 @@ public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
try
{
// Check if key combination is already in use (excluding current shortcut)
- var isInUse = await _keyboardShortcutService.IsKeyCombinationInUseAsync(result.Key, result.ModifierKeys, shortcut.Id);
+ var isInUse = await _keyboardShortcutService.IsKeyCombinationInUseAsync(result.Mode, result.Key, result.ModifierKeys, shortcut.Id);
if (isInUse)
{
await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
@@ -125,9 +126,10 @@ public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
// Update existing shortcut
var updatedShortcut = shortcut.ToEntity();
+ updatedShortcut.Mode = result.Mode;
updatedShortcut.Key = result.Key;
updatedShortcut.ModifierKeys = result.ModifierKeys;
- updatedShortcut.MailOperation = result.MailOperation;
+ updatedShortcut.Action = result.Action;
await _keyboardShortcutService.SaveKeyboardShortcutAsync(updatedShortcut);
await LoadShortcutsAsync();
diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs
index fea6a408..be5b3e31 100644
--- a/Wino.Mail.ViewModels/ComposePageViewModel.cs
+++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs
@@ -18,6 +18,7 @@ using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Extensions;
using Wino.Core.Services;
@@ -38,6 +39,18 @@ public partial class ComposePageViewModel : MailBaseViewModel,
public Func> GetHTMLBodyFunction;
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Mail)
+ return;
+
+ if (args.Action == KeyboardShortcutAction.Send)
+ {
+ await SendAsync();
+ args.Handled = true;
+ }
+ }
+
// When we send the message or discard it, we need to block the mime update
// Update is triggered when we leave the page.
private bool isUpdatingMimeBlocked = false;
diff --git a/Wino.Mail.ViewModels/MailAppShellViewModel.cs b/Wino.Mail.ViewModels/MailAppShellViewModel.cs
index 5965ba47..b4a4ca3f 100644
--- a/Wino.Mail.ViewModels/MailAppShellViewModel.cs
+++ b/Wino.Mail.ViewModels/MailAppShellViewModel.cs
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.MenuItems;
using Wino.Core.Domain.Models.Folders;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
@@ -935,6 +936,18 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
}
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Mail)
+ return;
+
+ if (args.Action == KeyboardShortcutAction.NewMail)
+ {
+ await HandleCreateNewMailAsync();
+ args.Handled = true;
+ }
+ }
+
// TODO: Handle by messaging.
diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs
index 7ae1f4bd..50e56849 100644
--- a/Wino.Mail.ViewModels/MailListPageViewModel.cs
+++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs
@@ -18,6 +18,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
+using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation;
@@ -72,6 +73,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private readonly IAccountService _accountService;
private readonly IMailDialogService _mailDialogService;
private readonly IMailService _mailService;
+ private readonly IMimeFileService _mimeFileService;
private readonly INotificationBuilder _notificationBuilder;
private readonly IFolderService _folderService;
private readonly IContextMenuItemService _contextMenuItemService;
@@ -165,6 +167,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IAccountService accountService,
IMailDialogService mailDialogService,
IMailService mailService,
+ IMimeFileService mimeFileService,
IStatePersistanceService statePersistenceService,
INotificationBuilder notificationBuilder,
IFolderService folderService,
@@ -179,6 +182,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
_accountService = accountService;
_mailDialogService = mailDialogService;
_mailService = mailService;
+ _mimeFileService = mimeFileService;
_folderService = folderService;
_contextMenuItemService = contextMenuItemService;
_winoRequestDelegator = winoRequestDelegator;
@@ -610,6 +614,87 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package);
+ public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
+ {
+ if (args.Handled || args.Mode != WinoApplicationMode.Mail)
+ return;
+
+ var targetItems = GetShortcutTargetItems().ToList();
+
+ switch (args.Action)
+ {
+ case KeyboardShortcutAction.ToggleReadUnread:
+ if (!targetItems.Any()) return;
+ await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.MarkAsRead, targetItems.Select(x => x.MailCopy), true));
+ args.Handled = true;
+ break;
+ case KeyboardShortcutAction.ToggleFlag:
+ if (!targetItems.Any()) return;
+ await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.SetFlag, targetItems.Select(x => x.MailCopy), true));
+ args.Handled = true;
+ break;
+ case KeyboardShortcutAction.ToggleArchive:
+ if (!targetItems.Any()) return;
+ await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.Archive, targetItems.Select(x => x.MailCopy), true));
+ args.Handled = true;
+ break;
+ case KeyboardShortcutAction.Delete:
+ if (!targetItems.Any()) return;
+ await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.SoftDelete, targetItems.Select(x => x.MailCopy)));
+ args.Handled = true;
+ break;
+ case KeyboardShortcutAction.Move:
+ if (!targetItems.Any()) return;
+ await ExecuteMailOperationAsync(new MailOperationPreperationRequest(MailOperation.Move, targetItems.Select(x => x.MailCopy)));
+ args.Handled = true;
+ break;
+ case KeyboardShortcutAction.Reply:
+ await CreateReplyDraftAsync(DraftCreationReason.Reply);
+ args.Handled = true;
+ break;
+ case KeyboardShortcutAction.ReplyAll:
+ await CreateReplyDraftAsync(DraftCreationReason.ReplyAll);
+ args.Handled = true;
+ break;
+ }
+ }
+
+ private IEnumerable GetShortcutTargetItems()
+ {
+ if (MailCollection.SelectedItemsCount > 0)
+ return MailCollection.SelectedItems.OfType();
+
+ if (_activeMailItem != null)
+ return [_activeMailItem];
+
+ return [];
+ }
+
+ private async Task CreateReplyDraftAsync(DraftCreationReason reason)
+ {
+ var targetMail = GetShortcutTargetItems().FirstOrDefault();
+ if (targetMail?.MailCopy == null || targetMail.MailCopy.FileId == Guid.Empty)
+ return;
+
+ var mimeInformation = await _mimeFileService.GetMimeMessageInformationAsync(targetMail.MailCopy.FileId, targetMail.MailCopy.AssignedAccount.Id);
+ if (mimeInformation?.MimeMessage == null)
+ return;
+
+ var draftOptions = new DraftCreationOptions
+ {
+ Reason = reason,
+ ReferencedMessage = new ReferencedMessage
+ {
+ MimeMessage = mimeInformation.MimeMessage,
+ MailCopy = targetMail.MailCopy
+ }
+ };
+
+ var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(targetMail.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
+ var draftPreparationRequest = new DraftPreparationRequest(targetMail.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, targetMail.MailCopy);
+ await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
+ }
+
public IEnumerable GetAvailableMailActions(IEnumerable contextMailItems)
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy));
diff --git a/Wino.Mail.WinUI/BasePage.cs b/Wino.Mail.WinUI/BasePage.cs
index 02229c36..1a6944c2 100644
--- a/Wino.Mail.WinUI/BasePage.cs
+++ b/Wino.Mail.WinUI/BasePage.cs
@@ -37,11 +37,14 @@ public partial class BasePage : Page, IRecipient
/// Unregister message recipients for this page. Override to unregister specific message types.
///
protected virtual void UnregisterRecipients() { }
+
+ public virtual CoreBaseViewModel? AssociatedViewModel => null;
}
public abstract class BasePage : BasePage where T : CoreBaseViewModel
{
public T ViewModel { get; } = WinoApplication.Current.Services.GetService() ?? throw new ArgumentException($"Can't resolve '{typeof(T)}' as view model.");
+ public override CoreBaseViewModel AssociatedViewModel => ViewModel;
protected BasePage()
{
diff --git a/Wino.Mail.WinUI/Dialogs/KeyboardShortcutDialog.xaml b/Wino.Mail.WinUI/Dialogs/KeyboardShortcutDialog.xaml
index 0d1ee4d4..6dc8f5e3 100644
--- a/Wino.Mail.WinUI/Dialogs/KeyboardShortcutDialog.xaml
+++ b/Wino.Mail.WinUI/Dialogs/KeyboardShortcutDialog.xaml
@@ -21,22 +21,40 @@
+
-
+
+
+
+
+
+
+
+
+
+ Text="{x:Bind domain:Translator.KeyboardShortcuts_Action}" />
+ ItemsSource="{x:Bind AvailableActions}"
+ SelectedItem="{x:Bind SelectedAction, Mode=TwoWay}">
-
+
@@ -44,7 +62,7 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
AvailableMailOperations { get; }
+ public List AvailableActions { get; private set; } = [];
- public MailOperationViewModel SelectedMailOperation { get; set; }
- public bool IsControlPressed { get; set; }
- public bool IsAltPressed { get; set; }
- public bool IsShiftPressed { get; set; }
- public bool IsWindowsPressed { get; set; }
+ public KeyboardShortcutActionViewModel SelectedAction { get; set; } = null!;
+ public WinoApplicationMode SelectedMode { get; set; } = WinoApplicationMode.Mail;
+ public bool IsMailModeSelected
+ {
+ get => SelectedMode == WinoApplicationMode.Mail;
+ set
+ {
+ if (!value || SelectedMode == WinoApplicationMode.Mail) return;
+ SelectedMode = WinoApplicationMode.Mail;
+ RefreshAvailableActions();
+ }
+ }
+
+ public bool IsCalendarModeSelected
+ {
+ get => SelectedMode == WinoApplicationMode.Calendar;
+ set
+ {
+ if (!value || SelectedMode == WinoApplicationMode.Calendar) return;
+ SelectedMode = WinoApplicationMode.Calendar;
+ RefreshAvailableActions();
+ }
+ }
+
+ private ModifierKeys _modifierKeys;
+ private string _key = string.Empty;
public KeyboardShortcutDialog()
{
InitializeComponent();
- AvailableMailOperations = GetAvailableMailOperations();
- SelectedMailOperation = AvailableMailOperations.FirstOrDefault()!;
+ RefreshAvailableActions();
}
public KeyboardShortcutDialog(KeyboardShortcut existingShortcut) : this()
{
if (existingShortcut != null)
{
- KeyInputTextBox.Text = existingShortcut.Key;
- SelectedMailOperation = AvailableMailOperations.FirstOrDefault(x => x.Operation == existingShortcut.MailOperation)!;
-
- var modifiers = existingShortcut.ModifierKeys;
- IsControlPressed = modifiers.HasFlag(ModifierKeys.Control);
- IsAltPressed = modifiers.HasFlag(ModifierKeys.Alt);
- IsShiftPressed = modifiers.HasFlag(ModifierKeys.Shift);
- IsWindowsPressed = modifiers.HasFlag(ModifierKeys.Windows);
-
- Title = "Edit Keyboard Shortcut";
+ SelectedMode = existingShortcut.Mode;
+ _modifierKeys = existingShortcut.ModifierKeys;
+ _key = existingShortcut.Key;
+ RefreshAvailableActions(existingShortcut.Action);
+ KeyInputTextBox.Text = BuildDisplayString(_key, _modifierKeys);
+ Title = Translator.KeyboardShortcuts_EditTitle;
}
}
@@ -51,61 +68,47 @@ public sealed partial class KeyboardShortcutDialog : ContentDialog
ErrorBorder.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
// Validate input
- if (string.IsNullOrWhiteSpace(KeyInputTextBox.Text))
+ if (string.IsNullOrWhiteSpace(_key))
{
- ShowError("Please enter a key for the shortcut.");
+ ShowError(Translator.KeyboardShortcuts_EnterKey);
args.Cancel = true;
return;
}
- if (SelectedMailOperation == null || SelectedMailOperation.Operation == MailOperation.None)
+ if (SelectedAction == null || SelectedAction.Action == KeyboardShortcutAction.None)
{
- ShowError("Please select a mail operation for the shortcut.");
+ ShowError(Translator.KeyboardShortcuts_SelectOperation);
args.Cancel = true;
return;
}
- // Get modifier keys
- var modifierKeys = GetSelectedModifierKeys();
-
- // Create successful result
- Result = KeyboardShortcutDialogResult.Success(KeyInputTextBox.Text, modifierKeys, SelectedMailOperation.Operation);
+ Result = KeyboardShortcutDialogResult.Success(SelectedMode, _key, _modifierKeys, SelectedAction.Action);
}
private void KeyInputTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
- // Clear error when user starts typing
ErrorBorder.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
- var key = e.Key.ToString();
+ _modifierKeys = GetCurrentModifierKeys();
+ var key = NormalizeKey(e.Key);
- // Update modifier states based on current key press
- IsControlPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Control).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down);
- IsAltPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Menu).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down);
- IsShiftPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Shift).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down);
- IsWindowsPressed = 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);
-
- // Set the key (ignore modifier keys themselves)
- if (key != "Control" && key != "Menu" && key != "Shift" && key != "LeftWindows" && key != "RightWindows")
+ if (!string.IsNullOrEmpty(key))
{
- KeyInputTextBox.Text = key;
+ _key = key;
}
- // Prevent the key from being processed further
- // e.Handled = true;
+ KeyInputTextBox.Text = string.IsNullOrEmpty(_key)
+ ? BuildDisplayString(string.Empty, _modifierKeys)
+ : BuildDisplayString(_key, _modifierKeys);
+
+ e.Handled = true;
}
- private ModifierKeys GetSelectedModifierKeys()
+ private void RefreshAvailableActions(KeyboardShortcutAction selectedAction = KeyboardShortcutAction.None)
{
- var modifiers = ModifierKeys.None;
-
- if (IsControlPressed) modifiers |= ModifierKeys.Control;
- if (IsAltPressed) modifiers |= ModifierKeys.Alt;
- if (IsShiftPressed) modifiers |= ModifierKeys.Shift;
- if (IsWindowsPressed) modifiers |= ModifierKeys.Windows;
-
- return modifiers;
+ AvailableActions = GetAvailableActions(SelectedMode);
+ SelectedAction = AvailableActions.FirstOrDefault(x => x.Action == selectedAction) ?? AvailableActions.FirstOrDefault()!;
+ Bindings.Update();
}
private void ShowError(string message)
@@ -114,32 +117,91 @@ public sealed partial class KeyboardShortcutDialog : ContentDialog
ErrorBorder.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
}
- private static List GetAvailableMailOperations()
+ private static List GetAvailableActions(WinoApplicationMode mode)
{
- var operations = new List();
-
- // Add commonly used mail operations that make sense for keyboard shortcuts
- var validOperations = new[]
+ KeyboardShortcutAction[] actions = mode switch
{
- MailOperation.Archive,
- MailOperation.UnArchive,
- MailOperation.SoftDelete,
- MailOperation.Move,
- MailOperation.MoveToJunk,
- MailOperation.SetFlag,
- MailOperation.ClearFlag,
- MailOperation.MarkAsRead,
- MailOperation.MarkAsUnread,
- MailOperation.Reply,
- MailOperation.ReplyAll,
- MailOperation.Forward
+ WinoApplicationMode.Mail =>
+ [
+ KeyboardShortcutAction.NewMail,
+ KeyboardShortcutAction.ToggleReadUnread,
+ KeyboardShortcutAction.ToggleFlag,
+ KeyboardShortcutAction.ToggleArchive,
+ KeyboardShortcutAction.Delete,
+ KeyboardShortcutAction.Move,
+ KeyboardShortcutAction.Reply,
+ KeyboardShortcutAction.ReplyAll,
+ KeyboardShortcutAction.Send
+ ],
+ WinoApplicationMode.Calendar =>
+ [
+ KeyboardShortcutAction.NewEvent,
+ KeyboardShortcutAction.Delete
+ ],
+ _ => []
};
- foreach (var operation in validOperations)
+ return actions
+ .Select(action => new KeyboardShortcutActionViewModel(mode, action))
+ .ToList();
+ }
+
+ 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))
{
- operations.Add(new MailOperationViewModel(operation));
+ modifiers |= ModifierKeys.Windows;
}
- return operations.OrderBy(x => x.DisplayName).ToList();
+ 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()
+ };
+ }
+
+ private static string BuildDisplayString(string key, ModifierKeys modifierKeys)
+ {
+ var parts = new List();
+
+ if (modifierKeys.HasFlag(ModifierKeys.Control))
+ parts.Add("Ctrl");
+ if (modifierKeys.HasFlag(ModifierKeys.Alt))
+ parts.Add("Alt");
+ if (modifierKeys.HasFlag(ModifierKeys.Shift))
+ parts.Add("Shift");
+ if (modifierKeys.HasFlag(ModifierKeys.Windows))
+ parts.Add("Win");
+ if (!string.IsNullOrEmpty(key))
+ parts.Add(key);
+
+ return string.Join("+", parts);
}
}
diff --git a/Wino.Mail.WinUI/Helpers/BreadcrumbNavigationHelper.cs b/Wino.Mail.WinUI/Helpers/BreadcrumbNavigationHelper.cs
new file mode 100644
index 00000000..6e50c8c9
--- /dev/null
+++ b/Wino.Mail.WinUI/Helpers/BreadcrumbNavigationHelper.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.ObjectModel;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media.Animation;
+using Wino.Core.Domain.Enums;
+using Wino.Mail.ViewModels.Data;
+using Wino.Messaging.Client.Navigation;
+
+namespace Wino.Helpers;
+
+public static class BreadcrumbNavigationHelper
+{
+ public static bool Navigate(
+ Frame frame,
+ ObservableCollection pageHistory,
+ BreadcrumbNavigationRequested message,
+ Func getPageType)
+ {
+ var pageType = getPageType(message.PageType);
+
+ if (pageType == null)
+ return false;
+
+ frame.Navigate(pageType, message.Parameter, new SlideNavigationTransitionInfo
+ {
+ Effect = SlideNavigationTransitionEffect.FromRight
+ });
+
+ SetActiveItem(pageHistory, null);
+ pageHistory.Add(new BreadcrumbNavigationItemViewModel(
+ message,
+ isActive: true,
+ stepNumber: pageHistory.Count + 1,
+ backStackDepth: frame.BackStack.Count + 1));
+
+ return true;
+ }
+
+ public static bool GoBack(
+ Frame frame,
+ ObservableCollection pageHistory,
+ NavigationTransitionEffect slideEffect)
+ {
+ if (!frame.CanGoBack || pageHistory.Count == 0)
+ return false;
+
+ pageHistory.RemoveAt(pageHistory.Count - 1);
+ frame.GoBack(new SlideNavigationTransitionInfo
+ {
+ Effect = slideEffect == NavigationTransitionEffect.FromLeft
+ ? SlideNavigationTransitionEffect.FromLeft
+ : SlideNavigationTransitionEffect.FromRight
+ });
+
+ SetActiveItem(pageHistory, pageHistory.Count > 0 ? pageHistory[^1] : null);
+ return true;
+ }
+
+ public static bool NavigateTo(
+ Frame frame,
+ ObservableCollection pageHistory,
+ int targetIndex)
+ {
+ if (targetIndex < 0 || targetIndex >= pageHistory.Count)
+ return false;
+
+ var activeIndex = GetActiveIndex(pageHistory);
+ if (activeIndex <= 0 || targetIndex >= activeIndex)
+ return false;
+
+ var targetItem = pageHistory[targetIndex];
+
+ while (frame.BackStack.Count > targetItem.BackStackDepth)
+ {
+ frame.BackStack.RemoveAt(frame.BackStack.Count - 1);
+ }
+
+ if (!frame.CanGoBack)
+ return false;
+
+ frame.GoBack(new SlideNavigationTransitionInfo
+ {
+ Effect = SlideNavigationTransitionEffect.FromLeft
+ });
+
+ while (pageHistory.Count > targetIndex + 1)
+ {
+ pageHistory.RemoveAt(pageHistory.Count - 1);
+ }
+
+ SetActiveItem(pageHistory, targetItem);
+ return true;
+ }
+
+ private static int GetActiveIndex(ObservableCollection pageHistory)
+ {
+ for (var i = 0; i < pageHistory.Count; i++)
+ {
+ if (pageHistory[i].IsActive)
+ return i;
+ }
+
+ return -1;
+ }
+
+ private static void SetActiveItem(
+ ObservableCollection pageHistory,
+ BreadcrumbNavigationItemViewModel? activeItem)
+ {
+ foreach (var item in pageHistory)
+ {
+ item.IsActive = ReferenceEquals(item, activeItem);
+ }
+ }
+}
diff --git a/Wino.Mail.WinUI/MailAppShell.xaml.cs b/Wino.Mail.WinUI/MailAppShell.xaml.cs
index 13fe2ce7..aa295da5 100644
--- a/Wino.Mail.WinUI/MailAppShell.xaml.cs
+++ b/Wino.Mail.WinUI/MailAppShell.xaml.cs
@@ -4,6 +4,7 @@ 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;
@@ -14,6 +15,7 @@ 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;
@@ -43,6 +45,7 @@ public sealed partial class MailAppShell : MailAppShellAbstract,
public MailAppShell() : base()
{
InitializeComponent();
+ PreviewKeyDown += OnPreviewKeyDown;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
@@ -315,6 +318,100 @@ public sealed partial class MailAppShell : MailAppShellAbstract,
}
}
+ 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 shortcutService = WinoApplication.Current.Services.GetRequiredService();
+ var shortcut = await shortcutService.GetShortcutForKeyAsync(WinoApplicationMode.Mail, 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.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 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();
diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs
index 158ecc47..0a48b77d 100644
--- a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs
+++ b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs
@@ -1,6 +1,12 @@
-using CommunityToolkit.Mvvm.Messaging;
+using System;
+using CommunityToolkit.Mvvm.Messaging;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models;
using Wino.Mail.Views.Abstract;
using Wino.Messaging.Client.Calendar;
@@ -17,14 +23,13 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
public CalendarAppShell()
{
InitializeComponent();
+ PreviewKeyDown += OnPreviewKeyDown;
- // Window.Current.SetTitleBar(DragArea);
ManageCalendarDisplayType(ViewModel.StatePersistenceService.CalendarDisplayType);
}
private void ManageCalendarDisplayType(Core.Domain.Enums.CalendarDisplayType displayType)
{
- // Go to different states based on the display type.
if (displayType == Core.Domain.Enums.CalendarDisplayType.Month)
{
VisualStateManager.GoToState(this, STATE_VerticalCalendar, false);
@@ -44,12 +49,6 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
ManageCalendarDisplayType(message.NewDisplayType);
}
- //private void ShellFrameContentNavigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
- // => RealAppBar.ShellFrameContent = (e.Content as BasePage).ShellContent;
-
- //private void AppBarBackButtonClicked(Core.UWP.Controls.WinoAppTitleBar sender, RoutedEventArgs args)
- // => ViewModel.NavigationService.GoBack();
-
protected override void RegisterRecipients()
{
base.RegisterRecipients();
@@ -63,4 +62,98 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
WeakReferenceMessenger.Default.Unregister(this);
}
+
+ 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 shortcutService = WinoApplication.Current.Services.GetRequiredService();
+ var shortcut = await shortcutService.GetShortcutForKeyAsync(WinoApplicationMode.Calendar, 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.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 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()
+ };
+ }
}
diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml
index 3295d3e9..4a9b29f4 100644
--- a/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml
+++ b/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml
@@ -470,8 +470,8 @@
-
+
@@ -560,7 +563,20 @@
-
+
+
+
+
+
+
+
+
+
+
+
isDarkMode ? WinoIconGlyph.LightEditor : WinoIconGlyph.DarkEditor;
+
+ public string GetEditorThemeToolTip(bool isDarkMode) => isDarkMode ? Translator.Composer_LightTheme : Translator.Composer_DarkTheme;
+
+ private void ToggleNotesEditorThemeClicked(object sender, RoutedEventArgs e)
+ {
+ NotesEditor.ToggleEditorTheme();
+ }
+
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
index 1dc13c29..195a86ba 100644
--- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
+++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
@@ -145,14 +145,13 @@
Visibility="{x:Bind ViewModel.IsDraftBusy, Mode=OneWay}">
-
-
-
-
-
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
index 155ce559..4819c7b3 100644
--- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
@@ -22,6 +22,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Reader;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
+using Wino.Mail.WinUI.Controls;
using Wino.Mail.WinUI.Extensions;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell;
@@ -43,6 +44,15 @@ public sealed partial class ComposePage : ComposePageAbstract,
InitializeComponent();
}
+ public WinoIconGlyph GetEditorThemeIcon(bool isDarkMode) => isDarkMode ? WinoIconGlyph.LightEditor : WinoIconGlyph.DarkEditor;
+
+ public string GetEditorThemeToolTip(bool isDarkMode) => isDarkMode ? Translator.Composer_LightTheme : Translator.Composer_DarkTheme;
+
+ private void ToggleEditorThemeClicked(object sender, RoutedEventArgs e)
+ {
+ WebViewEditor.ToggleEditorTheme();
+ }
+
private async void GlobalFocusManagerGotFocus(object? sender, FocusManagerGotFocusEventArgs e)
{
// In order to delegate cursor to the inner editor for WebView2.
diff --git a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs
index 855c2e64..d2587e5d 100644
--- a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs
@@ -52,7 +52,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
private IStatePersistanceService StatePersistenceService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}");
private IKeyPressService KeyPressService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}");
- private IKeyboardShortcutService KeyboardShortcutService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception($"Can't resolve {nameof(IKeyboardShortcutService)}");
public MailListPage()
{
InitializeComponent();
@@ -671,19 +670,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
else
{
- // Check keyboard shortcuts from service.
- ModifierKeys modifiers = args.Modifiers.ToDomainModifierKeys();
-
- var operation = await KeyboardShortcutService.GetMailOperationForKeyAsync(args.Key.ToString(), modifiers);
-
- if (operation != null)
- {
- ViewModel.ExecuteMailOperationCommand.Execute(operation);
- }
- else
- {
- args.Handled = false;
- }
+ args.Handled = false;
}
}
diff --git a/Wino.Mail.WinUI/Views/ManageAccountsPage.xaml.cs b/Wino.Mail.WinUI/Views/ManageAccountsPage.xaml.cs
index 8c3238b2..20ef8f9e 100644
--- a/Wino.Mail.WinUI/Views/ManageAccountsPage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/ManageAccountsPage.xaml.cs
@@ -3,9 +3,9 @@ using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
-using MoreLinq;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
+using Wino.Helpers;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.WinUI.Views.Abstract;
using Wino.Messaging.Client.Navigation;
@@ -42,7 +42,7 @@ public sealed partial class ManageAccountsPage : ManageAccountsPageAbstract,
AccountPagesFrame.Navigated += AccountPagesFrameNavigated;
var initialRequest = new BreadcrumbNavigationRequested(Translator.MenuManageAccounts, WinoPage.AccountManagementPage);
- PageHistory.Add(new BreadcrumbNavigationItemViewModel(initialRequest, true));
+ PageHistory.Add(new BreadcrumbNavigationItemViewModel(initialRequest, true, backStackDepth: AccountPagesFrame.BackStack.Count + 1));
var accountManagementPageType = ViewModel.NavigationService.GetPageType(WinoPage.AccountManagementPage);
@@ -69,15 +69,7 @@ public sealed partial class ManageAccountsPage : ManageAccountsPageAbstract,
void IRecipient.Receive(BreadcrumbNavigationRequested message)
{
- var pageType = ViewModel.NavigationService.GetPageType(message.PageType);
-
- if (pageType == null) return;
-
- AccountPagesFrame.Navigate(pageType, message.Parameter, new SlideNavigationTransitionInfo() { Effect = Microsoft.UI.Xaml.Media.Animation.SlideNavigationTransitionEffect.FromRight });
-
- PageHistory.ForEach(a => a.IsActive = false);
-
- PageHistory.Add(new BreadcrumbNavigationItemViewModel(message, true));
+ BreadcrumbNavigationHelper.Navigate(AccountPagesFrame, PageHistory, message, ViewModel.NavigationService.GetPageType);
UpdateWindowTitle();
}
@@ -89,40 +81,20 @@ public sealed partial class ManageAccountsPage : ManageAccountsPageAbstract,
private void GoBackFrame(Core.Domain.Enums.NavigationTransitionEffect slideEffect)
{
- if (AccountPagesFrame.CanGoBack)
- {
- PageHistory.RemoveAt(PageHistory.Count - 1);
+ if (!BreadcrumbNavigationHelper.GoBack(AccountPagesFrame, PageHistory, slideEffect))
+ return;
- var winuiEffect = slideEffect switch
- {
- Core.Domain.Enums.NavigationTransitionEffect.FromLeft => Microsoft.UI.Xaml.Media.Animation.SlideNavigationTransitionEffect.FromLeft,
- _ => Microsoft.UI.Xaml.Media.Animation.SlideNavigationTransitionEffect.FromRight,
- };
-
- AccountPagesFrame.GoBack(new SlideNavigationTransitionInfo() { Effect = winuiEffect });
-
- // Set the new last item as active
- if (PageHistory.Count > 0)
- {
- PageHistory.ForEach(a => a.IsActive = false);
- PageHistory[PageHistory.Count - 1].IsActive = true;
- }
-
- // Update back button visibility after navigation
- ViewModel.StatePersistenceService.IsManageAccountsNavigating = AccountPagesFrame.CanGoBack;
- UpdateWindowTitle();
- }
+ ViewModel.StatePersistenceService.IsManageAccountsNavigating = AccountPagesFrame.CanGoBack;
+ UpdateWindowTitle();
}
private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args)
{
- var clickedPageHistory = PageHistory[args.Index];
+ if (!BreadcrumbNavigationHelper.NavigateTo(AccountPagesFrame, PageHistory, args.Index))
+ return;
- // Trigger GoBack repeatedly until we reach the clicked breadcrumb item
- while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedPageHistory)
- {
- ViewModel.NavigationService.GoBack();
- }
+ ViewModel.StatePersistenceService.IsManageAccountsNavigating = AccountPagesFrame.CanGoBack;
+ UpdateWindowTitle();
}
public void Receive(BackBreadcrumNavigationRequested message)
diff --git a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml
index 17bb7d32..880f2bc2 100644
--- a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml
+++ b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml
@@ -221,6 +221,17 @@
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/Settings/KeyboardShortcutsPage.xaml b/Wino.Mail.WinUI/Views/Settings/KeyboardShortcutsPage.xaml
index 8f9fefc0..6a674f5b 100644
--- a/Wino.Mail.WinUI/Views/Settings/KeyboardShortcutsPage.xaml
+++ b/Wino.Mail.WinUI/Views/Settings/KeyboardShortcutsPage.xaml
@@ -52,19 +52,28 @@
+
+ Text="{x:Bind ActionDisplayName}" />
+
+
+ ToolTipService.ToolTip="{x:Bind domain:Translator.Buttons_Edit}" />
+ ToolTipService.ToolTip="{x:Bind domain:Translator.Buttons_Delete}" />
diff --git a/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs b/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs
index a8c35f95..a3ab2ea8 100644
--- a/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/SettingsPage.xaml.cs
@@ -3,9 +3,9 @@ using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
-using MoreLinq;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
+using Wino.Helpers;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation;
using Wino.Views.Abstract;
@@ -35,7 +35,7 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
SettingsFrame.Navigate(typeof(SettingOptionsPage), null, new SuppressNavigationTransitionInfo());
var initialRequest = new BreadcrumbNavigationRequested(Translator.MenuSettings, WinoPage.SettingOptionsPage);
- PageHistory.Add(new BreadcrumbNavigationItemViewModel(initialRequest, true));
+ PageHistory.Add(new BreadcrumbNavigationItemViewModel(initialRequest, true, backStackDepth: SettingsFrame.BackStack.Count + 1));
if (e.Parameter is WinoPage parameterPage)
{
@@ -99,15 +99,7 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
void IRecipient.Receive(BreadcrumbNavigationRequested message)
{
- var pageType = ViewModel.NavigationService.GetPageType(message.PageType);
-
- if (pageType == null) return;
-
- SettingsFrame.Navigate(pageType, message.Parameter, new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight });
-
- PageHistory.ForEach(a => a.IsActive = false);
-
- PageHistory.Add(new BreadcrumbNavigationItemViewModel(message, true));
+ BreadcrumbNavigationHelper.Navigate(SettingsFrame, PageHistory, message, ViewModel.NavigationService.GetPageType);
UpdateWindowTitle();
}
@@ -119,40 +111,20 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
private void GoBackFrame(Core.Domain.Enums.NavigationTransitionEffect slideEffect)
{
- if (SettingsFrame.CanGoBack)
- {
- PageHistory.RemoveAt(PageHistory.Count - 1);
+ if (!BreadcrumbNavigationHelper.GoBack(SettingsFrame, PageHistory, slideEffect))
+ return;
- var winuiEffect = slideEffect switch
- {
- Core.Domain.Enums.NavigationTransitionEffect.FromLeft => Microsoft.UI.Xaml.Media.Animation.SlideNavigationTransitionEffect.FromLeft,
- _ => Microsoft.UI.Xaml.Media.Animation.SlideNavigationTransitionEffect.FromRight,
- };
-
- SettingsFrame.GoBack(new SlideNavigationTransitionInfo() { Effect = winuiEffect });
-
- // Set the new last item as active
- if (PageHistory.Count > 0)
- {
- PageHistory.ForEach(a => a.IsActive = false);
- PageHistory[PageHistory.Count - 1].IsActive = true;
- }
-
- // Update back button visibility after navigation
- ViewModel.StatePersistenceService.IsSettingsNavigating = SettingsFrame.CanGoBack;
- UpdateWindowTitle();
- }
+ ViewModel.StatePersistenceService.IsSettingsNavigating = SettingsFrame.CanGoBack;
+ UpdateWindowTitle();
}
private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args)
{
- var clickedPageHistory = PageHistory[args.Index];
+ if (!BreadcrumbNavigationHelper.NavigateTo(SettingsFrame, PageHistory, args.Index))
+ return;
- // Trigger GoBack repeatedly until we reach the clicked breadcrumb item
- while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedPageHistory)
- {
- ViewModel.NavigationService.GoBack();
- }
+ ViewModel.StatePersistenceService.IsSettingsNavigating = SettingsFrame.CanGoBack;
+ UpdateWindowTitle();
}
public void Receive(BackBreadcrumNavigationRequested message)
diff --git a/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml.cs b/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml.cs
index be051b59..ef41e685 100644
--- a/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml.cs
@@ -3,8 +3,8 @@ using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
-using MoreLinq;
using Wino.Core.Domain.Enums;
+using Wino.Helpers;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.WinUI.Views.Abstract;
using Wino.Messaging.Client.Navigation;
@@ -45,16 +45,7 @@ public sealed partial class WelcomeHostPage : WelcomeHostPageAbstract,
public void Receive(BreadcrumbNavigationRequested message)
{
- var pageType = ViewModel.NavigationService.GetPageType(message.PageType);
- if (pageType == null) return;
-
- WizardFrame.Navigate(pageType, message.Parameter, new SlideNavigationTransitionInfo
- {
- Effect = SlideNavigationTransitionEffect.FromRight
- });
-
- PageHistory.ForEach(a => a.IsActive = false);
- PageHistory.Add(new BreadcrumbNavigationItemViewModel(message, isActive: true, stepNumber: PageHistory.Count + 1));
+ BreadcrumbNavigationHelper.Navigate(WizardFrame, PageHistory, message, ViewModel.NavigationService.GetPageType);
}
public void Receive(BackBreadcrumNavigationRequested message)
@@ -64,33 +55,11 @@ public sealed partial class WelcomeHostPage : WelcomeHostPageAbstract,
private void GoBackFrame()
{
- if (!WizardFrame.CanGoBack) return;
-
- PageHistory.RemoveAt(PageHistory.Count - 1);
- WizardFrame.GoBack(new SlideNavigationTransitionInfo
- {
- Effect = SlideNavigationTransitionEffect.FromLeft
- });
-
- if (PageHistory.Count > 0)
- {
- PageHistory.ForEach(a => a.IsActive = false);
- PageHistory[PageHistory.Count - 1].IsActive = true;
- }
+ BreadcrumbNavigationHelper.GoBack(WizardFrame, PageHistory, NavigationTransitionEffect.FromLeft);
}
private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args)
{
- var clickedItem = PageHistory[args.Index];
- var currentActive = PageHistory.FirstOrDefault(a => a.IsActive);
-
- // Only allow navigating backwards (clicking items before current)
- if (currentActive == null || args.Index >= PageHistory.IndexOf(currentActive))
- return;
-
- while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedItem && WizardFrame.CanGoBack)
- {
- GoBackFrame();
- }
+ BreadcrumbNavigationHelper.NavigateTo(WizardFrame, PageHistory, args.Index);
}
}
diff --git a/Wino.Services/DatabaseService.cs b/Wino.Services/DatabaseService.cs
index b2dfb7eb..e2f631cb 100644
--- a/Wino.Services/DatabaseService.cs
+++ b/Wino.Services/DatabaseService.cs
@@ -5,6 +5,7 @@ using SQLite;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Services;
@@ -74,6 +75,8 @@ public class DatabaseService : IDatabaseService
private async Task EnsureSchemaUpgradesAsync()
{
+ await EnsureKeyboardShortcutSchemaAsync().ConfigureAwait(false);
+
var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false);
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid)))
@@ -139,6 +142,44 @@ public class DatabaseService : IDatabaseService
}
}
+ private async Task EnsureKeyboardShortcutSchemaAsync()
+ {
+ var keyboardShortcutColumns = await Connection.GetTableInfoAsync(nameof(KeyboardShortcut)).ConfigureAwait(false);
+
+ if (!keyboardShortcutColumns.Any(c => c.Name == nameof(KeyboardShortcut.Mode)))
+ {
+ await Connection
+ .ExecuteAsync($"ALTER TABLE {nameof(KeyboardShortcut)} ADD COLUMN {nameof(KeyboardShortcut.Mode)} INTEGER NOT NULL DEFAULT 0")
+ .ConfigureAwait(false);
+ }
+
+ if (!keyboardShortcutColumns.Any(c => c.Name == nameof(KeyboardShortcut.Action)))
+ {
+ await Connection
+ .ExecuteAsync($"ALTER TABLE {nameof(KeyboardShortcut)} ADD COLUMN {nameof(KeyboardShortcut.Action)} INTEGER NOT NULL DEFAULT 0")
+ .ConfigureAwait(false);
+
+ await Connection.ExecuteAsync($@"
+UPDATE {nameof(KeyboardShortcut)}
+SET {nameof(KeyboardShortcut.Action)} =
+ CASE
+ WHEN MailOperation = {(int)MailOperation.Archive} THEN {(int)KeyboardShortcutAction.ToggleArchive}
+ WHEN MailOperation = {(int)MailOperation.UnArchive} THEN {(int)KeyboardShortcutAction.ToggleArchive}
+ WHEN MailOperation = {(int)MailOperation.SoftDelete} THEN {(int)KeyboardShortcutAction.Delete}
+ WHEN MailOperation = {(int)MailOperation.HardDelete} THEN {(int)KeyboardShortcutAction.Delete}
+ WHEN MailOperation = {(int)MailOperation.Move} THEN {(int)KeyboardShortcutAction.Move}
+ WHEN MailOperation = {(int)MailOperation.SetFlag} THEN {(int)KeyboardShortcutAction.ToggleFlag}
+ WHEN MailOperation = {(int)MailOperation.ClearFlag} THEN {(int)KeyboardShortcutAction.ToggleFlag}
+ WHEN MailOperation = {(int)MailOperation.MarkAsRead} THEN {(int)KeyboardShortcutAction.ToggleReadUnread}
+ WHEN MailOperation = {(int)MailOperation.MarkAsUnread} THEN {(int)KeyboardShortcutAction.ToggleReadUnread}
+ WHEN MailOperation = {(int)MailOperation.Reply} THEN {(int)KeyboardShortcutAction.Reply}
+ WHEN MailOperation = {(int)MailOperation.ReplyAll} THEN {(int)KeyboardShortcutAction.ReplyAll}
+ WHEN MailOperation = {(int)MailOperation.Forward} THEN {(int)KeyboardShortcutAction.Reply}
+ ELSE {(int)KeyboardShortcutAction.None}
+ END").ConfigureAwait(false);
+ }
+ }
+
private async Task EnsureIndexesAsync()
{
// Mail indexes
diff --git a/Wino.Services/KeyboardShortcutService.cs b/Wino.Services/KeyboardShortcutService.cs
index c3896dff..1f5ce61b 100644
--- a/Wino.Services/KeyboardShortcutService.cs
+++ b/Wino.Services/KeyboardShortcutService.cs
@@ -10,7 +10,7 @@ using Wino.Services.Extensions;
namespace Wino.Services;
///
-/// Service for managing keyboard shortcuts for mail operations.
+/// Service for managing keyboard shortcuts for mail and calendar actions.
///
public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutService
{
@@ -24,7 +24,7 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
public async Task> GetKeyboardShortcutsAsync()
{
return await Connection.QueryAsync(
- "SELECT * FROM KeyboardShortcut ORDER BY MailOperation");
+ "SELECT * FROM KeyboardShortcut ORDER BY Mode, Action");
}
///
@@ -33,7 +33,7 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
public async Task> GetEnabledKeyboardShortcutsAsync()
{
return await Connection.QueryAsync(
- "SELECT * FROM KeyboardShortcut WHERE IsEnabled = ? ORDER BY MailOperation",
+ "SELECT * FROM KeyboardShortcut WHERE IsEnabled = ? ORDER BY Mode, Action",
true);
}
@@ -65,34 +65,33 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
}
///
- /// Gets the mail operation for the given key combination.
+ /// Gets the shortcut for the given key combination.
///
- public async Task GetMailOperationForKeyAsync(string key, ModifierKeys modifierKeys)
+ public async Task GetShortcutForKeyAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys)
{
- const string query = "SELECT * FROM KeyboardShortcut WHERE Key = ? AND ModifierKeys = ? AND IsEnabled = ? LIMIT 1";
- var shortcut = await Connection.FindWithQueryAsync(query, key, (int)modifierKeys, 1);
- return shortcut?.MailOperation;
+ const string query = "SELECT * FROM KeyboardShortcut WHERE Mode = ? AND Key = ? AND ModifierKeys = ? AND IsEnabled = ? LIMIT 1";
+ return await Connection.FindWithQueryAsync(query, (int)mode, key, (int)modifierKeys, 1);
}
///
/// Checks if a key combination is already assigned to another shortcut.
///
- public async Task IsKeyCombinationInUseAsync(string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null)
+ public async Task IsKeyCombinationInUseAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null)
{
string query;
KeyboardShortcut shortcut;
-
+
if (excludeShortcutId.HasValue)
{
- query = "SELECT * FROM KeyboardShortcut WHERE Key = ? AND ModifierKeys = ? AND Id != ? LIMIT 1";
- shortcut = await Connection.FindWithQueryAsync(query, key, (int)modifierKeys, excludeShortcutId.Value);
+ query = "SELECT * FROM KeyboardShortcut WHERE Mode = ? AND Key = ? AND ModifierKeys = ? AND Id != ? LIMIT 1";
+ shortcut = await Connection.FindWithQueryAsync(query, (int)mode, key, (int)modifierKeys, excludeShortcutId.Value);
}
else
{
- query = "SELECT * FROM KeyboardShortcut WHERE Key = ? AND ModifierKeys = ? LIMIT 1";
- shortcut = await Connection.FindWithQueryAsync(query, key, (int)modifierKeys);
+ query = "SELECT * FROM KeyboardShortcut WHERE Mode = ? AND Key = ? AND ModifierKeys = ? LIMIT 1";
+ shortcut = await Connection.FindWithQueryAsync(query, (int)mode, key, (int)modifierKeys);
}
-
+
return shortcut != null;
}
@@ -106,7 +105,7 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
foreach (var shortcut in defaultShortcuts)
{
// Only create if it doesn't exist already
- var exists = await IsKeyCombinationInUseAsync(shortcut.Key, shortcut.ModifierKeys);
+ var exists = await IsKeyCombinationInUseAsync(shortcut.Mode, shortcut.Key, shortcut.ModifierKeys);
if (!exists)
{
await SaveKeyboardShortcutAsync(shortcut);
@@ -138,15 +137,17 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(),
Key = "Delete",
ModifierKeys = ModifierKeys.None,
- MailOperation = MailOperation.SoftDelete,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.Delete,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
- Key = "Delete",
- ModifierKeys = ModifierKeys.Shift,
- MailOperation = MailOperation.HardDelete,
+ Key = "N",
+ ModifierKeys = ModifierKeys.Control,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.NewMail,
IsEnabled = true
},
new KeyboardShortcut
@@ -154,7 +155,8 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(),
Key = "A",
ModifierKeys = ModifierKeys.Control,
- MailOperation = MailOperation.Archive,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.ToggleArchive,
IsEnabled = true
},
new KeyboardShortcut
@@ -162,15 +164,8 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(),
Key = "R",
ModifierKeys = ModifierKeys.Control,
- MailOperation = MailOperation.MarkAsRead,
- IsEnabled = true
- },
- new KeyboardShortcut
- {
- Id = Guid.NewGuid(),
- Key = "U",
- ModifierKeys = ModifierKeys.Control,
- MailOperation = MailOperation.MarkAsUnread,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.ToggleReadUnread,
IsEnabled = true
},
new KeyboardShortcut
@@ -178,23 +173,8 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(),
Key = "F",
ModifierKeys = ModifierKeys.Control,
- MailOperation = MailOperation.SetFlag,
- IsEnabled = true
- },
- new KeyboardShortcut
- {
- Id = Guid.NewGuid(),
- Key = "F",
- ModifierKeys = ModifierKeys.Control | ModifierKeys.Shift,
- MailOperation = MailOperation.ClearFlag,
- IsEnabled = true
- },
- new KeyboardShortcut
- {
- Id = Guid.NewGuid(),
- Key = "J",
- ModifierKeys = ModifierKeys.Control,
- MailOperation = MailOperation.MoveToJunk,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.ToggleFlag,
IsEnabled = true
},
new KeyboardShortcut
@@ -202,9 +182,37 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(),
Key = "M",
ModifierKeys = ModifierKeys.Control,
- MailOperation = MailOperation.Move,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.Move,
+ IsEnabled = true
+ },
+ new KeyboardShortcut
+ {
+ Id = Guid.NewGuid(),
+ Key = "R",
+ ModifierKeys = ModifierKeys.Control,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.Reply,
+ IsEnabled = true
+ },
+ new KeyboardShortcut
+ {
+ Id = Guid.NewGuid(),
+ Key = "R",
+ ModifierKeys = ModifierKeys.Control | ModifierKeys.Shift,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.ReplyAll,
+ IsEnabled = true
+ },
+ new KeyboardShortcut
+ {
+ Id = Guid.NewGuid(),
+ Key = "Enter",
+ ModifierKeys = ModifierKeys.Control,
+ Mode = WinoApplicationMode.Mail,
+ Action = KeyboardShortcutAction.Send,
IsEnabled = true
}
};
}
-}
\ No newline at end of file
+}