Improved keyboad shortcuts.

This commit is contained in:
Burak Kaan Köse
2026-03-08 13:21:42 +01:00
parent c1568d33e6
commit 15400d4096
35 changed files with 979 additions and 336 deletions
@@ -17,6 +17,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.ViewModels; 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] [RelayCommand]
@@ -14,6 +14,7 @@ using Serilog;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces; using Wino.Calendar.ViewModels.Interfaces;
using Wino.Calendar.ViewModels.Messages; using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain;
using Wino.Core.Domain.Collections; using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -21,6 +22,7 @@ using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies; using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels; using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
@@ -135,6 +137,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
private readonly INativeAppService _nativeAppService; private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService; private readonly IPreferencesService _preferencesService;
private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IMailDialogService _dialogService;
// Store latest rendered options. // Store latest rendered options.
private CalendarDisplayType _currentDisplayType; private CalendarDisplayType _currentDisplayType;
@@ -156,7 +159,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
INativeAppService nativeAppService, INativeAppService nativeAppService,
IAccountCalendarStateService accountCalendarStateService, IAccountCalendarStateService accountCalendarStateService,
IPreferencesService preferencesService, IPreferencesService preferencesService,
IWinoRequestDelegator winoRequestDelegator) IWinoRequestDelegator winoRequestDelegator,
IMailDialogService dialogService)
{ {
StatePersistanceService = statePersistanceService; StatePersistanceService = statePersistanceService;
AccountCalendarStateService = accountCalendarStateService; AccountCalendarStateService = accountCalendarStateService;
@@ -167,6 +171,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
_nativeAppService = nativeAppService; _nativeAppService = nativeAppService;
_preferencesService = preferencesService; _preferencesService = preferencesService;
_winoRequestDelegator = winoRequestDelegator; _winoRequestDelegator = winoRequestDelegator;
_dialogService = dialogService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
@@ -175,6 +180,35 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
RegisterRecipients(); 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() protected override void RegisterRecipients()
{ {
base.RegisterRecipients(); base.RegisterRecipients();
@@ -15,6 +15,7 @@ using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Core.ViewModels; 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] [RelayCommand]
private Task JoinOnlineAsync() private Task JoinOnlineAsync()
{ {
@@ -12,6 +12,11 @@ public class KeyboardShortcut
[PrimaryKey] [PrimaryKey]
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// The application mode this shortcut applies to.
/// </summary>
public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail;
/// <summary> /// <summary>
/// The key combination string (e.g., "D", "Delete", "F1"). /// The key combination string (e.g., "D", "Delete", "F1").
/// </summary> /// </summary>
@@ -23,9 +28,9 @@ public class KeyboardShortcut
public ModifierKeys ModifierKeys { get; set; } public ModifierKeys ModifierKeys { get; set; }
/// <summary> /// <summary>
/// The mail operation this shortcut triggers. /// The shortcut action this shortcut triggers.
/// </summary> /// </summary>
public MailOperation MailOperation { get; set; } public KeyboardShortcutAction Action { get; set; }
/// <summary> /// <summary>
/// Whether this shortcut is enabled. /// Whether this shortcut is enabled.
@@ -55,6 +60,6 @@ public class KeyboardShortcut
modifierText += "Win+"; modifierText += "Win+";
return modifierText + Key; return modifierText + Key;
} }
} }
} }
@@ -0,0 +1,16 @@
namespace Wino.Core.Domain.Enums;
public enum KeyboardShortcutAction
{
None,
NewMail,
ToggleReadUnread,
ToggleFlag,
ToggleArchive,
Delete,
Move,
Reply,
ReplyAll,
Send,
NewEvent
}
@@ -37,21 +37,23 @@ public interface IKeyboardShortcutService
Task DeleteKeyboardShortcutAsync(Guid shortcutId); Task DeleteKeyboardShortcutAsync(Guid shortcutId);
/// <summary> /// <summary>
/// Gets the mail operation for the given key combination. /// Gets the keyboard shortcut for the given key combination in a specific mode.
/// </summary> /// </summary>
/// <param name="mode">The application mode to search within.</param>
/// <param name="key">The pressed key.</param> /// <param name="key">The pressed key.</param>
/// <param name="modifierKeys">The modifier keys pressed.</param> /// <param name="modifierKeys">The modifier keys pressed.</param>
/// <returns>The mail operation if found, otherwise null.</returns> /// <returns>The matching shortcut if found, otherwise null.</returns>
Task<MailOperation?> GetMailOperationForKeyAsync(string key, ModifierKeys modifierKeys); Task<KeyboardShortcut> GetShortcutForKeyAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys);
/// <summary> /// <summary>
/// Checks if a key combination is already assigned to another shortcut. /// Checks if a key combination is already assigned to another shortcut.
/// </summary> /// </summary>
/// <param name="mode">The application mode to check within.</param>
/// <param name="key">The key to check.</param> /// <param name="key">The key to check.</param>
/// <param name="modifierKeys">The modifier keys to check.</param> /// <param name="modifierKeys">The modifier keys to check.</param>
/// <param name="excludeShortcutId">Optional ID to exclude from the check (for updates).</param> /// <param name="excludeShortcutId">Optional ID to exclude from the check (for updates).</param>
/// <returns>True if the combination is already used, false otherwise.</returns> /// <returns>True if the combination is already used, false otherwise.</returns>
Task<bool> IsKeyCombinationInUseAsync(string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null); Task<bool> IsKeyCombinationInUseAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null);
/// <summary> /// <summary>
/// Creates default keyboard shortcuts for common mail operations. /// Creates default keyboard shortcuts for common mail operations.
@@ -62,4 +64,4 @@ public interface IKeyboardShortcutService
/// Resets all shortcuts to defaults. /// Resets all shortcuts to defaults.
/// </summary> /// </summary>
Task ResetToDefaultShortcutsAsync(); Task ResetToDefaultShortcutsAsync();
} }
@@ -12,6 +12,11 @@ public class KeyboardShortcutDialogResult
/// </summary> /// </summary>
public bool IsSuccess { get; set; } public bool IsSuccess { get; set; }
/// <summary>
/// The application mode selected by the user.
/// </summary>
public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail;
/// <summary> /// <summary>
/// The key combination entered by the user. /// The key combination entered by the user.
/// </summary> /// </summary>
@@ -23,21 +28,22 @@ public class KeyboardShortcutDialogResult
public ModifierKeys ModifierKeys { get; set; } public ModifierKeys ModifierKeys { get; set; }
/// <summary> /// <summary>
/// The mail operation selected by the user. /// The shortcut action selected by the user.
/// </summary> /// </summary>
public MailOperation MailOperation { get; set; } public KeyboardShortcutAction Action { get; set; }
/// <summary> /// <summary>
/// Creates a successful result. /// Creates a successful result.
/// </summary> /// </summary>
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 return new KeyboardShortcutDialogResult
{ {
IsSuccess = true, IsSuccess = true,
Mode = mode,
Key = key, Key = key,
ModifierKeys = modifierKeys, ModifierKeys = modifierKeys,
MailOperation = mailOperation Action = action
}; };
} }
@@ -51,4 +57,4 @@ public class KeyboardShortcutDialogResult
IsSuccess = false IsSuccess = false
}; };
} }
} }
@@ -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; }
}
@@ -362,19 +362,29 @@
"KeyboardShortcuts_FailedToReset": "Failed to reset keyboard shortcuts.", "KeyboardShortcuts_FailedToReset": "Failed to reset keyboard shortcuts.",
"KeyboardShortcuts_FailedToUpdate": "Failed to update keyboard shortcuts", "KeyboardShortcuts_FailedToUpdate": "Failed to update keyboard shortcuts",
"KeyboardShortcuts_MailoperationAction": "Action", "KeyboardShortcuts_MailoperationAction": "Action",
"KeyboardShortcuts_Action": "Action",
"KeyboardShortcuts_FailedToLoad": "Failed to load keyboard shortcuts.", "KeyboardShortcuts_FailedToLoad": "Failed to load keyboard shortcuts.",
"KeyboardShortcuts_EnterKeyForShortcut": "Please enter a key for the shortcut.", "KeyboardShortcuts_EnterKeyForShortcut": "Please enter a key for the shortcut.",
"KeyboardShortcuts_SelectOperationForShortcut": "Please an action to perform 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_ShortcutInUse": "This shortcut is already in use by another s hortcut.",
"KeyboardShortcuts_FailedToSave": "Failed to save the shortcut.", "KeyboardShortcuts_FailedToSave": "Failed to save the shortcut.",
"KeyboardShortcuts_FailedToDelete": "Failed to delete 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_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_Add": "Add shortcut",
"KeyboardShortcuts_EditTitle": "Edit Keyboard Shortcut",
"KeyboardShortcuts_ResetToDefaults": "Reset to Defaults", "KeyboardShortcuts_ResetToDefaults": "Reset to Defaults",
"KeyboardShortcuts_PressKeysHere": "Press keys here...", "KeyboardShortcuts_PressKeysHere": "Press keys here...",
"KeyboardShortcuts_KeyCombination": "Key Combination", "KeyboardShortcuts_KeyCombination": "Key Combination",
"KeyboardShortcuts_FocusArea": "Focus the field above and press the desired key combination", "KeyboardShortcuts_FocusArea": "Focus the field above and press the desired key combination",
"KeyboardShortcuts_Modifiers": "Modifier Keys", "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.", "ImageRenderingDisabled": "Image rendering is disabled for this message.",
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method", "ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security", "ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
namespace Wino.Core.ViewModels; namespace Wino.Core.ViewModels;
@@ -40,6 +41,8 @@ public class CoreBaseViewModel : ObservableRecipient, INavigationAware
public virtual void OnPageLoaded() { } public virtual void OnPageLoaded() { }
public virtual Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) => Task.CompletedTask;
public Task ExecuteUIThread(Action action) public Task ExecuteUIThread(Action action)
{ {
if (action == null) return Task.CompletedTask; if (action == null) return Task.CompletedTask;
@@ -13,13 +13,16 @@ public partial class BreadcrumbNavigationItemViewModel : ObservableObject
public int StepNumber { get; set; } public int StepNumber { get; set; }
public int BackStackDepth { get; set; }
public BreadcrumbNavigationRequested Request { 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; Request = request;
Title = request.PageTitle; Title = request.PageTitle;
IsActive = isActive; IsActive = isActive;
StepNumber = stepNumber; StepNumber = stepNumber;
BackStackDepth = backStackDepth;
} }
} }
@@ -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;
}
@@ -15,9 +15,10 @@ public partial class KeyboardShortcutViewModel : ObservableObject
public partial bool IsEnabled { get; set; } public partial bool IsEnabled { get; set; }
public Guid Id { get; } public Guid Id { get; }
public WinoApplicationMode Mode { get; }
public string Key { get; } public string Key { get; }
public ModifierKeys ModifierKeys { get; } public ModifierKeys ModifierKeys { get; }
public MailOperation MailOperation { get; } public KeyboardShortcutAction Action { get; }
public DateTime CreatedAt { get; } public DateTime CreatedAt { get; }
public string DisplayName 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 get
{ {
return MailOperation switch return Action switch
{ {
MailOperation.Archive => Translator.MailOperation_Archive, KeyboardShortcutAction.NewMail => Translator.MenuNewMail,
MailOperation.UnArchive => Translator.MailOperation_Unarchive, KeyboardShortcutAction.ToggleReadUnread => Translator.KeyboardShortcuts_ActionToggleReadUnread,
MailOperation.SoftDelete => Translator.MailOperation_Delete, KeyboardShortcutAction.ToggleFlag => Translator.KeyboardShortcuts_ActionToggleFlag,
MailOperation.Move => Translator.MailOperation_Move, KeyboardShortcutAction.ToggleArchive => Translator.KeyboardShortcuts_ActionToggleArchive,
MailOperation.MoveToJunk => Translator.MailOperation_MoveJunk, KeyboardShortcutAction.Delete => Translator.Buttons_Delete,
MailOperation.SetFlag => Translator.MailOperation_SetFlag, KeyboardShortcutAction.Move => Translator.MailOperation_Move,
MailOperation.ClearFlag => Translator.MailOperation_ClearFlag, KeyboardShortcutAction.Reply => Translator.MailOperation_Reply,
MailOperation.MarkAsRead => Translator.MailOperation_MarkAsRead, KeyboardShortcutAction.ReplyAll => Translator.MailOperation_ReplyAll,
MailOperation.MarkAsUnread => Translator.MailOperation_MarkAsUnread, KeyboardShortcutAction.Send => Translator.Buttons_Send,
MailOperation.Reply => Translator.MailOperation_Reply, KeyboardShortcutAction.NewEvent => Translator.CalendarEventCompose_NewEventButton,
MailOperation.ReplyAll => Translator.MailOperation_ReplyAll, _ => Action.ToString()
MailOperation.Forward => Translator.MailOperation_Forward,
_ => MailOperation.ToString()
}; };
} }
} }
@@ -64,9 +70,10 @@ public partial class KeyboardShortcutViewModel : ObservableObject
public KeyboardShortcutViewModel(KeyboardShortcut shortcut) public KeyboardShortcutViewModel(KeyboardShortcut shortcut)
{ {
Id = shortcut.Id; Id = shortcut.Id;
Mode = shortcut.Mode;
Key = shortcut.Key; Key = shortcut.Key;
ModifierKeys = shortcut.ModifierKeys; ModifierKeys = shortcut.ModifierKeys;
MailOperation = shortcut.MailOperation; Action = shortcut.Action;
CreatedAt = shortcut.CreatedAt; CreatedAt = shortcut.CreatedAt;
IsEnabled = shortcut.IsEnabled; IsEnabled = shortcut.IsEnabled;
} }
@@ -76,9 +83,10 @@ public partial class KeyboardShortcutViewModel : ObservableObject
return new KeyboardShortcut return new KeyboardShortcut
{ {
Id = Id, Id = Id,
Mode = Mode,
Key = Key, Key = Key,
ModifierKeys = ModifierKeys, ModifierKeys = ModifierKeys,
MailOperation = MailOperation, Action = Action,
CreatedAt = CreatedAt, CreatedAt = CreatedAt,
IsEnabled = IsEnabled IsEnabled = IsEnabled
}; };
@@ -70,7 +70,7 @@ public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
try try
{ {
// Check if key combination is already in use // 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) if (isInUse)
{ {
await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error); await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
@@ -80,9 +80,10 @@ public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
// Create new shortcut // Create new shortcut
var shortcut = new KeyboardShortcut var shortcut = new KeyboardShortcut
{ {
Mode = result.Mode,
Key = result.Key, Key = result.Key,
ModifierKeys = result.ModifierKeys, ModifierKeys = result.ModifierKeys,
MailOperation = result.MailOperation, Action = result.Action,
IsEnabled = true IsEnabled = true
}; };
@@ -116,7 +117,7 @@ public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
try try
{ {
// Check if key combination is already in use (excluding current shortcut) // 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) if (isInUse)
{ {
await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error); await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
@@ -125,9 +126,10 @@ public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
// Update existing shortcut // Update existing shortcut
var updatedShortcut = shortcut.ToEntity(); var updatedShortcut = shortcut.ToEntity();
updatedShortcut.Mode = result.Mode;
updatedShortcut.Key = result.Key; updatedShortcut.Key = result.Key;
updatedShortcut.ModifierKeys = result.ModifierKeys; updatedShortcut.ModifierKeys = result.ModifierKeys;
updatedShortcut.MailOperation = result.MailOperation; updatedShortcut.Action = result.Action;
await _keyboardShortcutService.SaveKeyboardShortcutAsync(updatedShortcut); await _keyboardShortcutService.SaveKeyboardShortcutAsync(updatedShortcut);
await LoadShortcutsAsync(); await LoadShortcutsAsync();
@@ -18,6 +18,7 @@ using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Extensions; using Wino.Core.Extensions;
using Wino.Core.Services; using Wino.Core.Services;
@@ -38,6 +39,18 @@ public partial class ComposePageViewModel : MailBaseViewModel,
public Func<Task<string>> GetHTMLBodyFunction; public Func<Task<string>> 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 // When we send the message or discard it, we need to block the mime update
// Update is triggered when we leave the page. // Update is triggered when we leave the page.
private bool isUpdatingMimeBlocked = false; private bool isUpdatingMimeBlocked = false;
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.MenuItems; using Wino.Core.Domain.MenuItems;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
@@ -935,6 +936,18 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest); 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. // TODO: Handle by messaging.
@@ -18,6 +18,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
@@ -72,6 +73,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IMailDialogService _mailDialogService; private readonly IMailDialogService _mailDialogService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IMimeFileService _mimeFileService;
private readonly INotificationBuilder _notificationBuilder; private readonly INotificationBuilder _notificationBuilder;
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
private readonly IContextMenuItemService _contextMenuItemService; private readonly IContextMenuItemService _contextMenuItemService;
@@ -165,6 +167,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IAccountService accountService, IAccountService accountService,
IMailDialogService mailDialogService, IMailDialogService mailDialogService,
IMailService mailService, IMailService mailService,
IMimeFileService mimeFileService,
IStatePersistanceService statePersistenceService, IStatePersistanceService statePersistenceService,
INotificationBuilder notificationBuilder, INotificationBuilder notificationBuilder,
IFolderService folderService, IFolderService folderService,
@@ -179,6 +182,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
_accountService = accountService; _accountService = accountService;
_mailDialogService = mailDialogService; _mailDialogService = mailDialogService;
_mailService = mailService; _mailService = mailService;
_mimeFileService = mimeFileService;
_folderService = folderService; _folderService = folderService;
_contextMenuItemService = contextMenuItemService; _contextMenuItemService = contextMenuItemService;
_winoRequestDelegator = winoRequestDelegator; _winoRequestDelegator = winoRequestDelegator;
@@ -610,6 +614,87 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package); 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<MailItemViewModel> GetShortcutTargetItems()
{
if (MailCollection.SelectedItemsCount > 0)
return MailCollection.SelectedItems.OfType<MailItemViewModel>();
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<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<MailItemViewModel> contextMailItems) public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<MailItemViewModel> contextMailItems)
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy)); => _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy));
+3
View File
@@ -37,11 +37,14 @@ public partial class BasePage : Page, IRecipient<LanguageChanged>
/// Unregister message recipients for this page. Override to unregister specific message types. /// Unregister message recipients for this page. Override to unregister specific message types.
/// </summary> /// </summary>
protected virtual void UnregisterRecipients() { } protected virtual void UnregisterRecipients() { }
public virtual CoreBaseViewModel? AssociatedViewModel => null;
} }
public abstract class BasePage<T> : BasePage where T : CoreBaseViewModel public abstract class BasePage<T> : BasePage where T : CoreBaseViewModel
{ {
public T ViewModel { get; } = WinoApplication.Current.Services.GetService<T>() ?? throw new ArgumentException($"Can't resolve '{typeof(T)}' as view model."); public T ViewModel { get; } = WinoApplication.Current.Services.GetService<T>() ?? throw new ArgumentException($"Can't resolve '{typeof(T)}' as view model.");
public override CoreBaseViewModel AssociatedViewModel => ViewModel;
protected BasePage() protected BasePage()
{ {
@@ -21,22 +21,40 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Mail Operation --> <StackPanel Grid.Row="1" Margin="0,0,0,20">
<TextBlock
Margin="0,0,0,8"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.KeyboardShortcuts_Mode}" />
<StackPanel Orientation="Horizontal" Spacing="16">
<RadioButton
Content="{x:Bind domain:Translator.KeyboardShortcuts_ModeMail}"
GroupName="ShortcutMode"
IsChecked="{x:Bind IsMailModeSelected, Mode=TwoWay}" />
<RadioButton
Content="{x:Bind domain:Translator.KeyboardShortcuts_ModeCalendar}"
GroupName="ShortcutMode"
IsChecked="{x:Bind IsCalendarModeSelected, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
<!-- Action -->
<StackPanel Grid.Row="0" Margin="0,0,0,20"> <StackPanel Grid.Row="0" Margin="0,0,0,20">
<TextBlock <TextBlock
Margin="0,0,0,4" Margin="0,0,0,4"
Style="{ThemeResource BodyStrongTextBlockStyle}" Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.KeyboardShortcuts_MailoperationAction}" /> Text="{x:Bind domain:Translator.KeyboardShortcuts_Action}" />
<ComboBox <ComboBox
x:Name="MailOperationComboBox" x:Name="ActionComboBox"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
ItemsSource="{x:Bind AvailableMailOperations}" ItemsSource="{x:Bind AvailableActions}"
SelectedItem="{x:Bind SelectedMailOperation, Mode=TwoWay}"> SelectedItem="{x:Bind SelectedAction, Mode=TwoWay}">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate x:DataType="data:MailOperationViewModel"> <DataTemplate x:DataType="data:KeyboardShortcutActionViewModel">
<TextBlock Text="{x:Bind DisplayName}" /> <TextBlock Text="{x:Bind DisplayName}" />
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
@@ -44,7 +62,7 @@
</StackPanel> </StackPanel>
<!-- Key Input --> <!-- Key Input -->
<StackPanel Grid.Row="1" Margin="0,0,0,20"> <StackPanel Grid.Row="2" Margin="0,0,0,20">
<TextBlock <TextBlock
Margin="0,0,0,4" Margin="0,0,0,4"
Style="{ThemeResource BodyStrongTextBlockStyle}" Style="{ThemeResource BodyStrongTextBlockStyle}"
@@ -61,31 +79,10 @@
Text="{x:Bind domain:Translator.KeyboardShortcuts_FocusArea}" /> Text="{x:Bind domain:Translator.KeyboardShortcuts_FocusArea}" />
</StackPanel> </StackPanel>
<!-- Modifiers -->
<StackPanel Grid.Row="2" Margin="0,0,0,20">
<TextBlock
Margin="0,0,0,8"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.KeyboardShortcuts_Modifiers}" />
<Border
Padding="16,12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="6">
<StackPanel Orientation="Horizontal" Spacing="16">
<CheckBox Content="Ctrl" IsChecked="{x:Bind IsControlPressed, Mode=TwoWay}" />
<CheckBox Content="Alt" IsChecked="{x:Bind IsAltPressed, Mode=TwoWay}" />
<CheckBox Content="Shift" IsChecked="{x:Bind IsShiftPressed, Mode=TwoWay}" />
<CheckBox Content="Win" IsChecked="{x:Bind IsWindowsPressed, Mode=TwoWay}" />
</StackPanel>
</Border>
</StackPanel>
<!-- Error Message --> <!-- Error Message -->
<Border <Border
x:Name="ErrorBorder" x:Name="ErrorBorder"
Grid.Row="3" Grid.Row="4"
Padding="12,8" Padding="12,8"
Background="{ThemeResource SystemFillColorCriticalBackgroundBrush}" Background="{ThemeResource SystemFillColorCriticalBackgroundBrush}"
CornerRadius="4" CornerRadius="4"
@@ -4,6 +4,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain;
using Wino.Core.Domain.Models; using Wino.Core.Domain.Models;
using Wino.Core.ViewModels.Data; using Wino.Core.ViewModels.Data;
@@ -13,35 +14,51 @@ public sealed partial class KeyboardShortcutDialog : ContentDialog
{ {
public KeyboardShortcutDialogResult Result { get; private set; } = KeyboardShortcutDialogResult.Canceled(); public KeyboardShortcutDialogResult Result { get; private set; } = KeyboardShortcutDialogResult.Canceled();
public List<MailOperationViewModel> AvailableMailOperations { get; } public List<KeyboardShortcutActionViewModel> AvailableActions { get; private set; } = [];
public MailOperationViewModel SelectedMailOperation { get; set; } public KeyboardShortcutActionViewModel SelectedAction { get; set; } = null!;
public bool IsControlPressed { get; set; } public WinoApplicationMode SelectedMode { get; set; } = WinoApplicationMode.Mail;
public bool IsAltPressed { get; set; } public bool IsMailModeSelected
public bool IsShiftPressed { get; set; } {
public bool IsWindowsPressed { get; set; } 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() public KeyboardShortcutDialog()
{ {
InitializeComponent(); InitializeComponent();
AvailableMailOperations = GetAvailableMailOperations(); RefreshAvailableActions();
SelectedMailOperation = AvailableMailOperations.FirstOrDefault()!;
} }
public KeyboardShortcutDialog(KeyboardShortcut existingShortcut) : this() public KeyboardShortcutDialog(KeyboardShortcut existingShortcut) : this()
{ {
if (existingShortcut != null) if (existingShortcut != null)
{ {
KeyInputTextBox.Text = existingShortcut.Key; SelectedMode = existingShortcut.Mode;
SelectedMailOperation = AvailableMailOperations.FirstOrDefault(x => x.Operation == existingShortcut.MailOperation)!; _modifierKeys = existingShortcut.ModifierKeys;
_key = existingShortcut.Key;
var modifiers = existingShortcut.ModifierKeys; RefreshAvailableActions(existingShortcut.Action);
IsControlPressed = modifiers.HasFlag(ModifierKeys.Control); KeyInputTextBox.Text = BuildDisplayString(_key, _modifierKeys);
IsAltPressed = modifiers.HasFlag(ModifierKeys.Alt); Title = Translator.KeyboardShortcuts_EditTitle;
IsShiftPressed = modifiers.HasFlag(ModifierKeys.Shift);
IsWindowsPressed = modifiers.HasFlag(ModifierKeys.Windows);
Title = "Edit Keyboard Shortcut";
} }
} }
@@ -51,61 +68,47 @@ public sealed partial class KeyboardShortcutDialog : ContentDialog
ErrorBorder.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed; ErrorBorder.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
// Validate input // 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; args.Cancel = true;
return; 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; args.Cancel = true;
return; return;
} }
// Get modifier keys Result = KeyboardShortcutDialogResult.Success(SelectedMode, _key, _modifierKeys, SelectedAction.Action);
var modifierKeys = GetSelectedModifierKeys();
// Create successful result
Result = KeyboardShortcutDialogResult.Success(KeyInputTextBox.Text, modifierKeys, SelectedMailOperation.Operation);
} }
private void KeyInputTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e) private void KeyInputTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{ {
// Clear error when user starts typing
ErrorBorder.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed; 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 if (!string.IsNullOrEmpty(key))
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")
{ {
KeyInputTextBox.Text = key; _key = key;
} }
// Prevent the key from being processed further KeyInputTextBox.Text = string.IsNullOrEmpty(_key)
// e.Handled = true; ? BuildDisplayString(string.Empty, _modifierKeys)
: BuildDisplayString(_key, _modifierKeys);
e.Handled = true;
} }
private ModifierKeys GetSelectedModifierKeys() private void RefreshAvailableActions(KeyboardShortcutAction selectedAction = KeyboardShortcutAction.None)
{ {
var modifiers = ModifierKeys.None; AvailableActions = GetAvailableActions(SelectedMode);
SelectedAction = AvailableActions.FirstOrDefault(x => x.Action == selectedAction) ?? AvailableActions.FirstOrDefault()!;
if (IsControlPressed) modifiers |= ModifierKeys.Control; Bindings.Update();
if (IsAltPressed) modifiers |= ModifierKeys.Alt;
if (IsShiftPressed) modifiers |= ModifierKeys.Shift;
if (IsWindowsPressed) modifiers |= ModifierKeys.Windows;
return modifiers;
} }
private void ShowError(string message) private void ShowError(string message)
@@ -114,32 +117,91 @@ public sealed partial class KeyboardShortcutDialog : ContentDialog
ErrorBorder.Visibility = Microsoft.UI.Xaml.Visibility.Visible; ErrorBorder.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
} }
private static List<MailOperationViewModel> GetAvailableMailOperations() private static List<KeyboardShortcutActionViewModel> GetAvailableActions(WinoApplicationMode mode)
{ {
var operations = new List<MailOperationViewModel>(); KeyboardShortcutAction[] actions = mode switch
// Add commonly used mail operations that make sense for keyboard shortcuts
var validOperations = new[]
{ {
MailOperation.Archive, WinoApplicationMode.Mail =>
MailOperation.UnArchive, [
MailOperation.SoftDelete, KeyboardShortcutAction.NewMail,
MailOperation.Move, KeyboardShortcutAction.ToggleReadUnread,
MailOperation.MoveToJunk, KeyboardShortcutAction.ToggleFlag,
MailOperation.SetFlag, KeyboardShortcutAction.ToggleArchive,
MailOperation.ClearFlag, KeyboardShortcutAction.Delete,
MailOperation.MarkAsRead, KeyboardShortcutAction.Move,
MailOperation.MarkAsUnread, KeyboardShortcutAction.Reply,
MailOperation.Reply, KeyboardShortcutAction.ReplyAll,
MailOperation.ReplyAll, KeyboardShortcutAction.Send
MailOperation.Forward ],
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<string>();
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);
} }
} }
@@ -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<BreadcrumbNavigationItemViewModel> pageHistory,
BreadcrumbNavigationRequested message,
Func<WinoPage, Type> 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<BreadcrumbNavigationItemViewModel> 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<BreadcrumbNavigationItemViewModel> 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<BreadcrumbNavigationItemViewModel> pageHistory)
{
for (var i = 0; i < pageHistory.Count; i++)
{
if (pageHistory[i].IsActive)
return i;
}
return -1;
}
private static void SetActiveItem(
ObservableCollection<BreadcrumbNavigationItemViewModel> pageHistory,
BreadcrumbNavigationItemViewModel? activeItem)
{
foreach (var item in pageHistory)
{
item.IsActive = ReferenceEquals(item, activeItem);
}
}
}
+97
View File
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Controls.Primitives;
@@ -14,6 +15,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
@@ -43,6 +45,7 @@ public sealed partial class MailAppShell : MailAppShellAbstract,
public MailAppShell() : base() public MailAppShell() : base()
{ {
InitializeComponent(); InitializeComponent();
PreviewKeyDown += OnPreviewKeyDown;
} }
protected override void OnNavigatedFrom(NavigationEventArgs e) 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<IKeyboardShortcutService>();
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() protected override void RegisterRecipients()
{ {
base.RegisterRecipients(); base.RegisterRecipients();
@@ -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;
using Microsoft.UI.Xaml.Controls; 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.Mail.Views.Abstract;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
@@ -17,14 +23,13 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
public CalendarAppShell() public CalendarAppShell()
{ {
InitializeComponent(); InitializeComponent();
PreviewKeyDown += OnPreviewKeyDown;
// Window.Current.SetTitleBar(DragArea);
ManageCalendarDisplayType(ViewModel.StatePersistenceService.CalendarDisplayType); ManageCalendarDisplayType(ViewModel.StatePersistenceService.CalendarDisplayType);
} }
private void ManageCalendarDisplayType(Core.Domain.Enums.CalendarDisplayType displayType) private void ManageCalendarDisplayType(Core.Domain.Enums.CalendarDisplayType displayType)
{ {
// Go to different states based on the display type.
if (displayType == Core.Domain.Enums.CalendarDisplayType.Month) if (displayType == Core.Domain.Enums.CalendarDisplayType.Month)
{ {
VisualStateManager.GoToState(this, STATE_VerticalCalendar, false); VisualStateManager.GoToState(this, STATE_VerticalCalendar, false);
@@ -44,12 +49,6 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
ManageCalendarDisplayType(message.NewDisplayType); 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() protected override void RegisterRecipients()
{ {
base.RegisterRecipients(); base.RegisterRecipients();
@@ -63,4 +62,98 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
WeakReferenceMessenger.Default.Unregister<CalendarDisplayTypeChangedMessage>(this); WeakReferenceMessenger.Default.Unregister<CalendarDisplayTypeChangedMessage>(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<IKeyboardShortcutService>();
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()
};
}
} }
@@ -470,8 +470,8 @@
<Border <Border
x:Name="AttachmentsPane" x:Name="AttachmentsPane"
Margin="0,8,0,0" Margin="0,8,0,0"
AllowDrop="True"
Padding="16" Padding="16"
AllowDrop="True"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{StaticResource ControlCornerRadius}" CornerRadius="{StaticResource ControlCornerRadius}"
DragLeave="AttachmentsPane_DragLeave" DragLeave="AttachmentsPane_DragLeave"
@@ -487,7 +487,10 @@
Style="{StaticResource TransparentActionButtonStyle}"> Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="14" Icon="AttachmentNew" /> <coreControls:WinoFontIcon FontSize="14" Icon="AttachmentNew" />
<TextBlock FontSize="18" FontWeight="SemiBold" Text="+" /> <TextBlock
FontSize="18"
FontWeight="SemiBold"
Text="+" />
</StackPanel> </StackPanel>
</Button> </Button>
@@ -560,7 +563,20 @@
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.CalendarEventCompose_Notes}" /> <TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.CalendarEventCompose_Notes}" />
</StackPanel> </StackPanel>
<!-- Notes Editor --> <!-- Notes Editor -->
<mailControls:EditorTabbedCommandBarControl CommandTarget="{x:Bind NotesEditor}" /> <mailControls:EditorTabbedCommandBarControl CommandTarget="{x:Bind NotesEditor}">
<mailControls:EditorTabbedCommandBarControl.PaneCustomContent>
<toolkit:TabbedCommandBarItem
CommandAlignment="Right"
IsDynamicOverflowEnabled="True"
OverflowButtonAlignment="Left">
<AppBarButton Click="ToggleNotesEditorThemeClicked" ToolTipService.ToolTip="{x:Bind GetEditorThemeToolTip(NotesEditor.IsEditorDarkMode), Mode=OneWay}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="{x:Bind GetEditorThemeIcon(NotesEditor.IsEditorDarkMode), Mode=OneWay}" />
</AppBarButton.Icon>
</AppBarButton>
</toolkit:TabbedCommandBarItem>
</mailControls:EditorTabbedCommandBarControl.PaneCustomContent>
</mailControls:EditorTabbedCommandBarControl>
<mailControls:WebViewEditorControl <mailControls:WebViewEditorControl
x:Name="NotesEditor" x:Name="NotesEditor"
MinHeight="500" MinHeight="500"
@@ -15,6 +15,7 @@ using Windows.Storage;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Messaging.Client.Shell; using Wino.Messaging.Client.Shell;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
using Wino.Mail.WinUI.Controls;
using Wino.Mail.WinUI.Views.Abstract; using Wino.Mail.WinUI.Views.Abstract;
namespace Wino.Calendar.Views; namespace Wino.Calendar.Views;
@@ -29,6 +30,15 @@ public sealed partial class CalendarEventComposePage : CalendarEventComposePageA
InitializeComponent(); 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 ToggleNotesEditorThemeClicked(object sender, RoutedEventArgs e)
{
NotesEditor.ToggleEditorTheme();
}
protected override async void OnNavigatedTo(NavigationEventArgs e) protected override async void OnNavigatedTo(NavigationEventArgs e)
{ {
base.OnNavigatedTo(e); base.OnNavigatedTo(e);
+7 -8
View File
@@ -145,14 +145,13 @@
Visibility="{x:Bind ViewModel.IsDraftBusy, Mode=OneWay}"> Visibility="{x:Bind ViewModel.IsDraftBusy, Mode=OneWay}">
<ProgressRing IsActive="True" /> <ProgressRing IsActive="True" />
</AppBarButton> </AppBarButton>
<AppBarToggleButton <AppBarButton
x:Name="EditorThemeToggleButton" Click="ToggleEditorThemeClicked"
IsChecked="{x:Bind WebViewEditor.IsEditorDarkMode, Mode=TwoWay}" ToolTipService.ToolTip="{x:Bind GetEditorThemeToolTip(WebViewEditor.IsEditorDarkMode), Mode=OneWay}">
ToolTipService.ToolTip="Toggle editor dark mode"> <AppBarButton.Icon>
<AppBarToggleButton.Icon> <coreControls:WinoFontIcon Icon="{x:Bind GetEditorThemeIcon(WebViewEditor.IsEditorDarkMode), Mode=OneWay}" />
<coreControls:WinoFontIcon Icon="DarkEditor" /> </AppBarButton.Icon>
</AppBarToggleButton.Icon> </AppBarButton>
</AppBarToggleButton>
<AppBarButton Command="{x:Bind ViewModel.DiscardCommand}" Label="{x:Bind domain:Translator.Buttons_Discard}"> <AppBarButton Command="{x:Bind ViewModel.DiscardCommand}" Label="{x:Bind domain:Translator.Buttons_Discard}">
<AppBarButton.Icon> <AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Delete" /> <coreControls:WinoFontIcon Icon="Delete" />
@@ -22,6 +22,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Reader;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI.Controls;
using Wino.Mail.WinUI.Extensions; using Wino.Mail.WinUI.Extensions;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell; using Wino.Messaging.Client.Shell;
@@ -43,6 +44,15 @@ public sealed partial class ComposePage : ComposePageAbstract,
InitializeComponent(); 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) private async void GlobalFocusManagerGotFocus(object? sender, FocusManagerGotFocusEventArgs e)
{ {
// In order to delegate cursor to the inner editor for WebView2. // In order to delegate cursor to the inner editor for WebView2.
@@ -52,7 +52,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
private IStatePersistanceService StatePersistenceService { get; } = WinoApplication.Current.Services.GetService<IStatePersistanceService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}"); private IStatePersistanceService StatePersistenceService { get; } = WinoApplication.Current.Services.GetService<IStatePersistanceService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}");
private IKeyPressService KeyPressService { get; } = WinoApplication.Current.Services.GetService<IKeyPressService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}"); private IKeyPressService KeyPressService { get; } = WinoApplication.Current.Services.GetService<IKeyPressService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}");
private IKeyboardShortcutService KeyboardShortcutService { get; } = WinoApplication.Current.Services.GetService<IKeyboardShortcutService>() ?? throw new Exception($"Can't resolve {nameof(IKeyboardShortcutService)}");
public MailListPage() public MailListPage()
{ {
InitializeComponent(); InitializeComponent();
@@ -671,19 +670,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
} }
else else
{ {
// Check keyboard shortcuts from service. args.Handled = false;
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;
}
} }
} }
@@ -3,9 +3,9 @@ using System.Linq;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using MoreLinq;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.WinUI.Views.Abstract; using Wino.Mail.WinUI.Views.Abstract;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
@@ -42,7 +42,7 @@ public sealed partial class ManageAccountsPage : ManageAccountsPageAbstract,
AccountPagesFrame.Navigated += AccountPagesFrameNavigated; AccountPagesFrame.Navigated += AccountPagesFrameNavigated;
var initialRequest = new BreadcrumbNavigationRequested(Translator.MenuManageAccounts, WinoPage.AccountManagementPage); 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); var accountManagementPageType = ViewModel.NavigationService.GetPageType(WinoPage.AccountManagementPage);
@@ -69,15 +69,7 @@ public sealed partial class ManageAccountsPage : ManageAccountsPageAbstract,
void IRecipient<BreadcrumbNavigationRequested>.Receive(BreadcrumbNavigationRequested message) void IRecipient<BreadcrumbNavigationRequested>.Receive(BreadcrumbNavigationRequested message)
{ {
var pageType = ViewModel.NavigationService.GetPageType(message.PageType); BreadcrumbNavigationHelper.Navigate(AccountPagesFrame, PageHistory, message, ViewModel.NavigationService.GetPageType);
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));
UpdateWindowTitle(); UpdateWindowTitle();
} }
@@ -89,40 +81,20 @@ public sealed partial class ManageAccountsPage : ManageAccountsPageAbstract,
private void GoBackFrame(Core.Domain.Enums.NavigationTransitionEffect slideEffect) private void GoBackFrame(Core.Domain.Enums.NavigationTransitionEffect slideEffect)
{ {
if (AccountPagesFrame.CanGoBack) if (!BreadcrumbNavigationHelper.GoBack(AccountPagesFrame, PageHistory, slideEffect))
{ return;
PageHistory.RemoveAt(PageHistory.Count - 1);
var winuiEffect = slideEffect switch ViewModel.StatePersistenceService.IsManageAccountsNavigating = AccountPagesFrame.CanGoBack;
{ UpdateWindowTitle();
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();
}
} }
private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args) 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 ViewModel.StatePersistenceService.IsManageAccountsNavigating = AccountPagesFrame.CanGoBack;
while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedPageHistory) UpdateWindowTitle();
{
ViewModel.NavigationService.GoBack();
}
} }
public void Receive(BackBreadcrumNavigationRequested message) public void Receive(BackBreadcrumNavigationRequested message)
@@ -221,6 +221,17 @@
<FontIcon Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" Glyph="&#xE787;" /> <FontIcon Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" Glyph="&#xE787;" />
</controls:SettingsCard.HeaderIcon> </controls:SettingsCard.HeaderIcon>
</controls:SettingsCard> </controls:SettingsCard>
<controls:SettingsCard
Click="SettingOptionClicked"
Description="{x:Bind domain:Translator.Settings_KeyboardShortcuts_Description}"
Header="{x:Bind domain:Translator.Settings_KeyboardShortcuts_Title}"
IsClickEnabled="True"
Tag="{x:Bind enums:WinoPage.KeyboardShortcutsPage}">
<controls:SettingsCard.HeaderIcon>
<FontIcon Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" Glyph="&#xE765;" />
</controls:SettingsCard.HeaderIcon>
</controls:SettingsCard>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</abstract:SettingOptionsPageAbstract> </abstract:SettingOptionsPageAbstract>
@@ -52,19 +52,28 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"> <StackPanel Grid.Column="0">
<TextBlock <TextBlock
Margin="0,0,0,4" Margin="0,0,0,4"
Style="{ThemeResource BodyStrongTextBlockStyle}" Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind MailOperationDisplayName}" /> Text="{x:Bind ActionDisplayName}" />
<TextBlock <TextBlock
Opacity="0.8" Opacity="0.8"
Style="{ThemeResource CaptionTextBlockStyle}" Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind DisplayName}" /> Text="{x:Bind DisplayName}" />
</StackPanel> </StackPanel>
<TextBlock
Grid.Column="1"
Margin="8,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind ModeDisplayName}" />
<Button <Button
Grid.Column="2" Grid.Column="2"
Margin="4,0" Margin="4,0"
@@ -73,7 +82,7 @@
Content="&#xE70F;" Content="&#xE70F;"
FontFamily="Segoe MDL2 Assets" FontFamily="Segoe MDL2 Assets"
Style="{ThemeResource SubtleButtonStyle}" Style="{ThemeResource SubtleButtonStyle}"
ToolTipService.ToolTip="Edit" /> ToolTipService.ToolTip="{x:Bind domain:Translator.Buttons_Edit}" />
<Button <Button
Grid.Column="3" Grid.Column="3"
@@ -83,7 +92,7 @@
Content="&#xE74D;" Content="&#xE74D;"
FontFamily="Segoe MDL2 Assets" FontFamily="Segoe MDL2 Assets"
Style="{ThemeResource SubtleButtonStyle}" Style="{ThemeResource SubtleButtonStyle}"
ToolTipService.ToolTip="Delete" /> ToolTipService.ToolTip="{x:Bind domain:Translator.Buttons_Delete}" />
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
+11 -39
View File
@@ -3,9 +3,9 @@ using System.Linq;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using MoreLinq;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
using Wino.Views.Abstract; using Wino.Views.Abstract;
@@ -35,7 +35,7 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
SettingsFrame.Navigate(typeof(SettingOptionsPage), null, new SuppressNavigationTransitionInfo()); SettingsFrame.Navigate(typeof(SettingOptionsPage), null, new SuppressNavigationTransitionInfo());
var initialRequest = new BreadcrumbNavigationRequested(Translator.MenuSettings, WinoPage.SettingOptionsPage); 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) if (e.Parameter is WinoPage parameterPage)
{ {
@@ -99,15 +99,7 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
void IRecipient<BreadcrumbNavigationRequested>.Receive(BreadcrumbNavigationRequested message) void IRecipient<BreadcrumbNavigationRequested>.Receive(BreadcrumbNavigationRequested message)
{ {
var pageType = ViewModel.NavigationService.GetPageType(message.PageType); BreadcrumbNavigationHelper.Navigate(SettingsFrame, PageHistory, message, ViewModel.NavigationService.GetPageType);
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));
UpdateWindowTitle(); UpdateWindowTitle();
} }
@@ -119,40 +111,20 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
private void GoBackFrame(Core.Domain.Enums.NavigationTransitionEffect slideEffect) private void GoBackFrame(Core.Domain.Enums.NavigationTransitionEffect slideEffect)
{ {
if (SettingsFrame.CanGoBack) if (!BreadcrumbNavigationHelper.GoBack(SettingsFrame, PageHistory, slideEffect))
{ return;
PageHistory.RemoveAt(PageHistory.Count - 1);
var winuiEffect = slideEffect switch ViewModel.StatePersistenceService.IsSettingsNavigating = SettingsFrame.CanGoBack;
{ UpdateWindowTitle();
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();
}
} }
private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args) 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 ViewModel.StatePersistenceService.IsSettingsNavigating = SettingsFrame.CanGoBack;
while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedPageHistory) UpdateWindowTitle();
{
ViewModel.NavigationService.GoBack();
}
} }
public void Receive(BackBreadcrumNavigationRequested message) public void Receive(BackBreadcrumNavigationRequested message)
+4 -35
View File
@@ -3,8 +3,8 @@ using System.Linq;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using MoreLinq;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.WinUI.Views.Abstract; using Wino.Mail.WinUI.Views.Abstract;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
@@ -45,16 +45,7 @@ public sealed partial class WelcomeHostPage : WelcomeHostPageAbstract,
public void Receive(BreadcrumbNavigationRequested message) public void Receive(BreadcrumbNavigationRequested message)
{ {
var pageType = ViewModel.NavigationService.GetPageType(message.PageType); BreadcrumbNavigationHelper.Navigate(WizardFrame, PageHistory, message, ViewModel.NavigationService.GetPageType);
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));
} }
public void Receive(BackBreadcrumNavigationRequested message) public void Receive(BackBreadcrumNavigationRequested message)
@@ -64,33 +55,11 @@ public sealed partial class WelcomeHostPage : WelcomeHostPageAbstract,
private void GoBackFrame() private void GoBackFrame()
{ {
if (!WizardFrame.CanGoBack) return; BreadcrumbNavigationHelper.GoBack(WizardFrame, PageHistory, NavigationTransitionEffect.FromLeft);
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;
}
} }
private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args) private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args)
{ {
var clickedItem = PageHistory[args.Index]; BreadcrumbNavigationHelper.NavigateTo(WizardFrame, 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();
}
} }
} }
+41
View File
@@ -5,6 +5,7 @@ using SQLite;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Services; namespace Wino.Services;
@@ -74,6 +75,8 @@ public class DatabaseService : IDatabaseService
private async Task EnsureSchemaUpgradesAsync() private async Task EnsureSchemaUpgradesAsync()
{ {
await EnsureKeyboardShortcutSchemaAsync().ConfigureAwait(false);
var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false); var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false);
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid))) 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() private async Task EnsureIndexesAsync()
{ {
// Mail indexes // Mail indexes
+57 -49
View File
@@ -10,7 +10,7 @@ using Wino.Services.Extensions;
namespace Wino.Services; namespace Wino.Services;
/// <summary> /// <summary>
/// Service for managing keyboard shortcuts for mail operations. /// Service for managing keyboard shortcuts for mail and calendar actions.
/// </summary> /// </summary>
public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutService public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutService
{ {
@@ -24,7 +24,7 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
public async Task<IEnumerable<KeyboardShortcut>> GetKeyboardShortcutsAsync() public async Task<IEnumerable<KeyboardShortcut>> GetKeyboardShortcutsAsync()
{ {
return await Connection.QueryAsync<KeyboardShortcut>( return await Connection.QueryAsync<KeyboardShortcut>(
"SELECT * FROM KeyboardShortcut ORDER BY MailOperation"); "SELECT * FROM KeyboardShortcut ORDER BY Mode, Action");
} }
/// <summary> /// <summary>
@@ -33,7 +33,7 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
public async Task<IEnumerable<KeyboardShortcut>> GetEnabledKeyboardShortcutsAsync() public async Task<IEnumerable<KeyboardShortcut>> GetEnabledKeyboardShortcutsAsync()
{ {
return await Connection.QueryAsync<KeyboardShortcut>( return await Connection.QueryAsync<KeyboardShortcut>(
"SELECT * FROM KeyboardShortcut WHERE IsEnabled = ? ORDER BY MailOperation", "SELECT * FROM KeyboardShortcut WHERE IsEnabled = ? ORDER BY Mode, Action",
true); true);
} }
@@ -65,34 +65,33 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
} }
/// <summary> /// <summary>
/// Gets the mail operation for the given key combination. /// Gets the shortcut for the given key combination.
/// </summary> /// </summary>
public async Task<MailOperation?> GetMailOperationForKeyAsync(string key, ModifierKeys modifierKeys) public async Task<KeyboardShortcut> GetShortcutForKeyAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys)
{ {
const string query = "SELECT * FROM KeyboardShortcut WHERE Key = ? AND ModifierKeys = ? AND IsEnabled = ? LIMIT 1"; const string query = "SELECT * FROM KeyboardShortcut WHERE Mode = ? AND Key = ? AND ModifierKeys = ? AND IsEnabled = ? LIMIT 1";
var shortcut = await Connection.FindWithQueryAsync<KeyboardShortcut>(query, key, (int)modifierKeys, 1); return await Connection.FindWithQueryAsync<KeyboardShortcut>(query, (int)mode, key, (int)modifierKeys, 1);
return shortcut?.MailOperation;
} }
/// <summary> /// <summary>
/// Checks if a key combination is already assigned to another shortcut. /// Checks if a key combination is already assigned to another shortcut.
/// </summary> /// </summary>
public async Task<bool> IsKeyCombinationInUseAsync(string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null) public async Task<bool> IsKeyCombinationInUseAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null)
{ {
string query; string query;
KeyboardShortcut shortcut; KeyboardShortcut shortcut;
if (excludeShortcutId.HasValue) if (excludeShortcutId.HasValue)
{ {
query = "SELECT * FROM KeyboardShortcut WHERE Key = ? AND ModifierKeys = ? AND Id != ? LIMIT 1"; query = "SELECT * FROM KeyboardShortcut WHERE Mode = ? AND Key = ? AND ModifierKeys = ? AND Id != ? LIMIT 1";
shortcut = await Connection.FindWithQueryAsync<KeyboardShortcut>(query, key, (int)modifierKeys, excludeShortcutId.Value); shortcut = await Connection.FindWithQueryAsync<KeyboardShortcut>(query, (int)mode, key, (int)modifierKeys, excludeShortcutId.Value);
} }
else else
{ {
query = "SELECT * FROM KeyboardShortcut WHERE Key = ? AND ModifierKeys = ? LIMIT 1"; query = "SELECT * FROM KeyboardShortcut WHERE Mode = ? AND Key = ? AND ModifierKeys = ? LIMIT 1";
shortcut = await Connection.FindWithQueryAsync<KeyboardShortcut>(query, key, (int)modifierKeys); shortcut = await Connection.FindWithQueryAsync<KeyboardShortcut>(query, (int)mode, key, (int)modifierKeys);
} }
return shortcut != null; return shortcut != null;
} }
@@ -106,7 +105,7 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
foreach (var shortcut in defaultShortcuts) foreach (var shortcut in defaultShortcuts)
{ {
// Only create if it doesn't exist already // 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) if (!exists)
{ {
await SaveKeyboardShortcutAsync(shortcut); await SaveKeyboardShortcutAsync(shortcut);
@@ -138,15 +137,17 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Key = "Delete", Key = "Delete",
ModifierKeys = ModifierKeys.None, ModifierKeys = ModifierKeys.None,
MailOperation = MailOperation.SoftDelete, Mode = WinoApplicationMode.Mail,
Action = KeyboardShortcutAction.Delete,
IsEnabled = true IsEnabled = true
}, },
new KeyboardShortcut new KeyboardShortcut
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Key = "Delete", Key = "N",
ModifierKeys = ModifierKeys.Shift, ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.HardDelete, Mode = WinoApplicationMode.Mail,
Action = KeyboardShortcutAction.NewMail,
IsEnabled = true IsEnabled = true
}, },
new KeyboardShortcut new KeyboardShortcut
@@ -154,7 +155,8 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Key = "A", Key = "A",
ModifierKeys = ModifierKeys.Control, ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.Archive, Mode = WinoApplicationMode.Mail,
Action = KeyboardShortcutAction.ToggleArchive,
IsEnabled = true IsEnabled = true
}, },
new KeyboardShortcut new KeyboardShortcut
@@ -162,15 +164,8 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Key = "R", Key = "R",
ModifierKeys = ModifierKeys.Control, ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.MarkAsRead, Mode = WinoApplicationMode.Mail,
IsEnabled = true Action = KeyboardShortcutAction.ToggleReadUnread,
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "U",
ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.MarkAsUnread,
IsEnabled = true IsEnabled = true
}, },
new KeyboardShortcut new KeyboardShortcut
@@ -178,23 +173,8 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Key = "F", Key = "F",
ModifierKeys = ModifierKeys.Control, ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.SetFlag, Mode = WinoApplicationMode.Mail,
IsEnabled = true Action = KeyboardShortcutAction.ToggleFlag,
},
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,
IsEnabled = true IsEnabled = true
}, },
new KeyboardShortcut new KeyboardShortcut
@@ -202,9 +182,37 @@ public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutSer
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Key = "M", Key = "M",
ModifierKeys = ModifierKeys.Control, 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 IsEnabled = true
} }
}; };
} }
} }