Keyboard shortcuts dialog.

This commit is contained in:
Burak Kaan Köse
2025-10-29 16:26:46 +01:00
parent abaab18eb7
commit b44fb5c45a
25 changed files with 1112 additions and 4 deletions
@@ -0,0 +1,60 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Shared;
/// <summary>
/// Represents a user-defined keyboard shortcut for mail operations.
/// </summary>
public class KeyboardShortcut
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// The key combination string (e.g., "D", "Delete", "F1").
/// </summary>
public string Key { get; set; }
/// <summary>
/// The modifier keys for this shortcut.
/// </summary>
public ModifierKeys ModifierKeys { get; set; }
/// <summary>
/// The mail operation this shortcut triggers.
/// </summary>
public MailOperation MailOperation { get; set; }
/// <summary>
/// Whether this shortcut is enabled.
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// When this shortcut was created.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// User-friendly display name for the shortcut.
/// </summary>
public string DisplayName
{
get
{
var modifierText = string.Empty;
if (ModifierKeys.HasFlag(ModifierKeys.Control))
modifierText += "Ctrl+";
if (ModifierKeys.HasFlag(ModifierKeys.Alt))
modifierText += "Alt+";
if (ModifierKeys.HasFlag(ModifierKeys.Shift))
modifierText += "Shift+";
if (ModifierKeys.HasFlag(ModifierKeys.Windows))
modifierText += "Win+";
return modifierText + Key;
}
}
}
+16
View File
@@ -0,0 +1,16 @@
using System;
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Defines keyboard modifier keys that can be used in keyboard shortcuts.
/// </summary>
[Flags]
public enum ModifierKeys
{
None = 0,
Control = 1,
Alt = 2,
Shift = 4,
Windows = 8
}
+1
View File
@@ -26,6 +26,7 @@ public enum WinoPage
SettingOptionsPage,
AliasManagementPage,
EditAccountDetailsPage,
KeyboardShortcutsPage,
// Calendar
CalendarPage,
CalendarSettingsPage,
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Service for managing keyboard shortcuts for mail operations.
/// </summary>
public interface IKeyboardShortcutService
{
/// <summary>
/// Gets all available keyboard shortcuts.
/// </summary>
/// <returns>Collection of keyboard shortcuts.</returns>
Task<IEnumerable<KeyboardShortcut>> GetKeyboardShortcutsAsync();
/// <summary>
/// Gets enabled keyboard shortcuts only.
/// </summary>
/// <returns>Collection of enabled keyboard shortcuts.</returns>
Task<IEnumerable<KeyboardShortcut>> GetEnabledKeyboardShortcutsAsync();
/// <summary>
/// Creates or updates a keyboard shortcut.
/// </summary>
/// <param name="shortcut">The keyboard shortcut to save.</param>
/// <returns>The saved keyboard shortcut.</returns>
Task<KeyboardShortcut> SaveKeyboardShortcutAsync(KeyboardShortcut shortcut);
/// <summary>
/// Deletes a keyboard shortcut.
/// </summary>
/// <param name="shortcutId">The ID of the shortcut to delete.</param>
Task DeleteKeyboardShortcutAsync(Guid shortcutId);
/// <summary>
/// Gets the mail operation for the given key combination.
/// </summary>
/// <param name="key">The pressed key.</param>
/// <param name="modifierKeys">The modifier keys pressed.</param>
/// <returns>The mail operation if found, otherwise null.</returns>
Task<MailOperation?> GetMailOperationForKeyAsync(string key, ModifierKeys modifierKeys);
/// <summary>
/// Checks if a key combination is already assigned to another shortcut.
/// </summary>
/// <param name="key">The key 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>
/// <returns>True if the combination is already used, false otherwise.</returns>
Task<bool> IsKeyCombinationInUseAsync(string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null);
/// <summary>
/// Creates default keyboard shortcuts for common mail operations.
/// </summary>
Task CreateDefaultShortcutsAsync();
/// <summary>
/// Resets all shortcuts to defaults.
/// </summary>
Task ResetToDefaultShortcutsAsync();
}
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.Interfaces;
@@ -49,4 +50,13 @@ public interface IMailDialogService : IDialogServiceBase
/// Presents a dialog to the user to show email source.
/// </summary>
Task ShowMessageSourceDialogAsync(string messageSource);
/// <summary>
/// Presents a dialog to the user for keyboard shortcut creation/modification.
/// </summary>
/// <param name="existingShortcut">Existing shortcut to edit, or null for new shortcut.</param>
/// <returns>Dialog result with shortcut information.</returns>
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
Task<KeyboardShortcutDialogResult> ShowKeyboardShortcutDialogAsync(KeyboardShortcut existingShortcut = null);
#pragma warning restore CS8625
}
@@ -0,0 +1,54 @@
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models;
/// <summary>
/// Result returned from keyboard shortcut dialog.
/// </summary>
public class KeyboardShortcutDialogResult
{
/// <summary>
/// Whether the dialog was completed successfully.
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// The key combination entered by the user.
/// </summary>
public string Key { get; set; } = string.Empty;
/// <summary>
/// The modifier keys selected by the user.
/// </summary>
public ModifierKeys ModifierKeys { get; set; }
/// <summary>
/// The mail operation selected by the user.
/// </summary>
public MailOperation MailOperation { get; set; }
/// <summary>
/// Creates a successful result.
/// </summary>
public static KeyboardShortcutDialogResult Success(string key, ModifierKeys modifierKeys, MailOperation mailOperation)
{
return new KeyboardShortcutDialogResult
{
IsSuccess = true,
Key = key,
ModifierKeys = modifierKeys,
MailOperation = mailOperation
};
}
/// <summary>
/// Creates a canceled result.
/// </summary>
public static KeyboardShortcutDialogResult Canceled()
{
return new KeyboardShortcutDialogResult
{
IsSuccess = false
};
}
}
@@ -229,6 +229,22 @@
"HoverActionOption_MoveJunk": "Move to Junk",
"HoverActionOption_ToggleFlag": "Flag / Unflag",
"HoverActionOption_ToggleRead": "Read / Unread",
"KeyboardShortcuts_FailedToReset": "Failed to reset keyboard shortcuts.",
"KeyboardShortcuts_FailedToUpdate": "Failed to update keyboard shortcuts",
"KeyboardShortcuts_MailoperationAction": "Action",
"KeyboardShortcuts_FailedToLoad": "Failed to load keyboard shortcuts.",
"KeyboardShortcuts_EnterKeyForShortcut": "Please enter a key for the shortcut.",
"KeyboardShortcuts_SelectOperationForShortcut": "Please an action to perform for the shortcut.",
"KeyboardShortcuts_ShortcutInUse": "This shortcut is already in use by another s hortcut.",
"KeyboardShortcuts_FailedToSave": "Failed to save the shortcut.",
"KeyboardShortcuts_FailedToDelete": "Failed to delete the shortcut.",
"KeyboardShortcuts_PageDescription": "Set up keyboard shortcuts for quick mail operations. Press keys while focused on the key input field to capture shortcuts.",
"KeyboardShortcuts_Add": "Add shortcut",
"KeyboardShortcuts_ResetToDefaults": "Reset to Defaults",
"KeyboardShortcuts_PressKeysHere": "Press keys here...",
"KeyboardShortcuts_KeyCombination": "Key Combination",
"KeyboardShortcuts_FocusArea": "Focus the field above and press the desired key combination",
"KeyboardShortcuts_Modifiers": "Modifier Keys",
"ImageRenderingDisabled": "Image rendering is disabled for this message.",
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
@@ -465,6 +481,8 @@
"SearchBarPlaceholder": "Search",
"SearchingIn": "Searching in",
"SearchPivotName": "Results",
"Settings_KeyboardShortcuts_Title": "Keyboard Shortcuts",
"Settings_KeyboardShortcuts_Description": "Manage keyboard shortcuts for quick actions on the mails.",
"SettingConfigureSpecialFolders_Button": "Configure",
"SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration",
"SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.",
@@ -0,0 +1,86 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
namespace Wino.Core.ViewModels.Data;
/// <summary>
/// ViewModel wrapper for KeyboardShortcut entity.
/// </summary>
public partial class KeyboardShortcutViewModel : ObservableObject
{
[ObservableProperty]
private bool isEnabled;
public Guid Id { get; }
public string Key { get; }
public ModifierKeys ModifierKeys { get; }
public MailOperation MailOperation { get; }
public DateTime CreatedAt { get; }
public string DisplayName
{
get
{
var modifierText = string.Empty;
if (ModifierKeys.HasFlag(ModifierKeys.Control))
modifierText += "Ctrl+";
if (ModifierKeys.HasFlag(ModifierKeys.Alt))
modifierText += "Alt+";
if (ModifierKeys.HasFlag(ModifierKeys.Shift))
modifierText += "Shift+";
if (ModifierKeys.HasFlag(ModifierKeys.Windows))
modifierText += "Win+";
return modifierText + Key;
}
}
public string MailOperationDisplayName
{
get
{
return MailOperation switch
{
MailOperation.Archive => "Archive",
MailOperation.UnArchive => "Unarchive",
MailOperation.SoftDelete => "Delete",
MailOperation.Move => "Move",
MailOperation.MoveToJunk => "Move to Junk",
MailOperation.SetFlag => "Set Flag",
MailOperation.ClearFlag => "Clear Flag",
MailOperation.MarkAsRead => "Mark as Read",
MailOperation.MarkAsUnread => "Mark as Unread",
MailOperation.Reply => "Reply",
MailOperation.ReplyAll => "Reply All",
MailOperation.Forward => "Forward",
_ => MailOperation.ToString()
};
}
}
public KeyboardShortcutViewModel(KeyboardShortcut shortcut)
{
Id = shortcut.Id;
Key = shortcut.Key;
ModifierKeys = shortcut.ModifierKeys;
MailOperation = shortcut.MailOperation;
CreatedAt = shortcut.CreatedAt;
IsEnabled = shortcut.IsEnabled;
}
public KeyboardShortcut ToEntity()
{
return new KeyboardShortcut
{
Id = Id,
Key = Key,
ModifierKeys = ModifierKeys,
MailOperation = MailOperation,
CreatedAt = CreatedAt,
IsEnabled = IsEnabled
};
}
}
@@ -0,0 +1,42 @@
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
namespace Wino.Core.ViewModels.Data;
/// <summary>
/// ViewModel for displaying mail operations in dropdowns/lists.
/// </summary>
public class MailOperationViewModel
{
public MailOperation Operation { get; }
public string DisplayName
{
get
{
return Operation switch
{
MailOperation.Archive => "Archive",
MailOperation.UnArchive => "Unarchive",
MailOperation.SoftDelete => "Delete",
MailOperation.Move => "Move",
MailOperation.MoveToJunk => "Move to Junk",
MailOperation.SetFlag => "Set Flag",
MailOperation.ClearFlag => "Clear Flag",
MailOperation.MarkAsRead => "Mark as Read",
MailOperation.MarkAsUnread => "Mark as Unread",
MailOperation.Reply => "Reply",
MailOperation.ReplyAll => "Reply All",
MailOperation.Forward => "Forward",
_ => Operation.ToString()
};
}
}
public MailOperationViewModel(MailOperation operation)
{
Operation = operation;
}
public override string ToString() => DisplayName;
}
@@ -0,0 +1,179 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels.Data;
namespace Wino.Core.ViewModels;
/// <summary>
/// ViewModel for managing keyboard shortcuts settings.
/// </summary>
public partial class KeyboardShortcutsPageViewModel : CoreBaseViewModel
{
private readonly IKeyboardShortcutService _keyboardShortcutService;
private readonly IMailDialogService _dialogService;
[ObservableProperty]
public partial ObservableCollection<KeyboardShortcutViewModel> Shortcuts { get; set; } = new();
public KeyboardShortcutsPageViewModel(IKeyboardShortcutService keyboardShortcutService,
IMailDialogService dialogService)
{
_keyboardShortcutService = keyboardShortcutService;
_dialogService = dialogService;
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
await LoadShortcutsAsync();
}
[RelayCommand]
private async Task LoadShortcutsAsync()
{
try
{
var keyboardShortcuts = await _keyboardShortcutService.GetKeyboardShortcutsAsync();
Shortcuts.Clear();
foreach (var shortcut in keyboardShortcuts)
{
Shortcuts.Add(new KeyboardShortcutViewModel(shortcut));
}
}
catch (Exception ex)
{
await _dialogService.ShowMessageAsync(
Translator.KeyboardShortcuts_FailedToLoad,
Translator.GeneralTitle_Error,
WinoCustomMessageDialogIcon.Error);
}
}
[RelayCommand]
private async Task StartAddingShortcutAsync()
{
var result = await _dialogService.ShowKeyboardShortcutDialogAsync();
if (result.IsSuccess)
{
try
{
// Check if key combination is already in use
var isInUse = await _keyboardShortcutService.IsKeyCombinationInUseAsync(result.Key, result.ModifierKeys, null);
if (isInUse)
{
await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
return;
}
// Create new shortcut
var shortcut = new KeyboardShortcut
{
Key = result.Key,
ModifierKeys = result.ModifierKeys,
MailOperation = result.MailOperation,
IsEnabled = true
};
await _keyboardShortcutService.SaveKeyboardShortcutAsync(shortcut);
await LoadShortcutsAsync();
}
catch (Exception ex)
{
await _dialogService.ShowMessageAsync(
Translator.KeyboardShortcuts_FailedToSave,
Translator.GeneralTitle_Error,
WinoCustomMessageDialogIcon.Error);
}
}
}
[RelayCommand]
private async Task StartEditingShortcutAsync(KeyboardShortcutViewModel shortcut)
{
if (shortcut == null) return;
var dialogService = _dialogService as IMailDialogService;
if (dialogService == null) return;
var existingShortcut = shortcut.ToEntity();
var result = await dialogService.ShowKeyboardShortcutDialogAsync(existingShortcut);
if (result.IsSuccess)
{
try
{
// Check if key combination is already in use (excluding current shortcut)
var isInUse = await _keyboardShortcutService.IsKeyCombinationInUseAsync(result.Key, result.ModifierKeys, shortcut.Id);
if (isInUse)
{
await _dialogService.ShowMessageAsync(Translator.KeyboardShortcuts_ShortcutInUse, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
return;
}
// Update existing shortcut
var updatedShortcut = shortcut.ToEntity();
updatedShortcut.Key = result.Key;
updatedShortcut.ModifierKeys = result.ModifierKeys;
updatedShortcut.MailOperation = result.MailOperation;
await _keyboardShortcutService.SaveKeyboardShortcutAsync(updatedShortcut);
await LoadShortcutsAsync();
}
catch (Exception ex)
{
await _dialogService.ShowMessageAsync(
Translator.KeyboardShortcuts_FailedToUpdate,
Translator.GeneralTitle_Error,
WinoCustomMessageDialogIcon.Error);
}
}
}
[RelayCommand]
private async Task DeleteShortcutAsync(KeyboardShortcutViewModel shortcut)
{
if (shortcut == null) return;
try
{
await _keyboardShortcutService.DeleteKeyboardShortcutAsync(shortcut.Id);
await LoadShortcutsAsync();
}
catch (Exception ex)
{
await _dialogService.ShowMessageAsync(
Translator.KeyboardShortcuts_FailedToDelete,
Translator.GeneralTitle_Error,
WinoCustomMessageDialogIcon.Error);
}
}
[RelayCommand]
private async Task ResetToDefaultsAsync()
{
try
{
await _keyboardShortcutService.ResetToDefaultShortcutsAsync();
await LoadShortcutsAsync();
}
catch (Exception ex)
{
await _dialogService.ShowMessageAsync(
Translator.KeyboardShortcuts_FailedToReset,
Translator.GeneralTitle_Error,
WinoCustomMessageDialogIcon.Error);
}
}
}
@@ -38,6 +38,7 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
WinoPage.LanguageTimePage => Translator.SettingsLanguageTime_Title,
WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title,
WinoPage.CalendarSettingsPage => Translator.SettingsCalendarSettings_Title,
WinoPage.KeyboardShortcutsPage => "Keyboard Shortcuts",
_ => throw new NotImplementedException()
};
+1
View File
@@ -40,5 +40,6 @@ public static class CoreUWPContainerSetup
services.AddTransient(typeof(AboutPageViewModel));
services.AddTransient(typeof(SettingsPageViewModel));
services.AddTransient(typeof(ManageAccountsPagePageViewModel));
services.AddTransient(typeof(KeyboardShortcutsPageViewModel));
}
}
@@ -0,0 +1,80 @@
<ContentDialog
x:Class="Wino.Dialogs.KeyboardShortcutDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:data="using:Wino.Core.ViewModels.Data"
xmlns:domain="using:Wino.Core.Domain"
xmlns:local="using:Wino.Dialogs"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{x:Bind domain:Translator.KeyboardShortcuts_Add}"
DefaultButton="Primary"
PrimaryButtonClick="SaveClicked"
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Save}"
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Cancel}"
Style="{StaticResource WinoDialogStyle}"
mc:Ignorable="d">
<Grid MinWidth="400" MinHeight="300">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Mail Operation -->
<StackPanel Grid.Row="0" Margin="0,0,0,16">
<TextBlock
Margin="0,0,0,4"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.KeyboardShortcuts_MailoperationAction}" />
<ComboBox
x:Name="MailOperationComboBox"
HorizontalAlignment="Stretch"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind AvailableMailOperations}"
SelectedItem="{x:Bind SelectedMailOperation, Mode=TwoWay}" />
</StackPanel>
<!-- Key Input -->
<StackPanel Grid.Row="1" Margin="0,0,0,16">
<TextBlock
Margin="0,0,0,4"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.KeyboardShortcuts_KeyCombination}" />
<TextBox
x:Name="KeyInputTextBox"
IsReadOnly="True"
PlaceholderText="{x:Bind domain:Translator.KeyboardShortcuts_PressKeysHere}"
PreviewKeyDown="KeyInputTextBox_PreviewKeyDown" />
<TextBlock
Margin="0,4,0,0"
Opacity="0.7"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.KeyboardShortcuts_FocusArea}" />
</StackPanel>
<!-- Modifiers -->
<StackPanel Grid.Row="2" Margin="0,0,0,16">
<TextBlock
Margin="0,0,0,8"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.KeyboardShortcuts_Modifiers}" />
<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>
</StackPanel>
<!-- Error Message -->
<TextBlock
x:Name="ErrorTextBlock"
Grid.Row="3"
Foreground="{ThemeResource SystemErrorTextColor}"
Style="{ThemeResource CaptionTextBlockStyle}"
Visibility="Collapsed" />
</Grid>
</ContentDialog>
@@ -0,0 +1,145 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models;
using Wino.Core.ViewModels.Data;
namespace Wino.Dialogs;
public sealed partial class KeyboardShortcutDialog : ContentDialog
{
public KeyboardShortcutDialogResult Result { get; private set; } = KeyboardShortcutDialogResult.Canceled();
public List<MailOperationViewModel> AvailableMailOperations { get; }
public MailOperationViewModel SelectedMailOperation { get; set; }
public bool IsControlPressed { get; set; }
public bool IsAltPressed { get; set; }
public bool IsShiftPressed { get; set; }
public bool IsWindowsPressed { get; set; }
public KeyboardShortcutDialog()
{
InitializeComponent();
AvailableMailOperations = GetAvailableMailOperations();
SelectedMailOperation = AvailableMailOperations.FirstOrDefault();
}
public KeyboardShortcutDialog(KeyboardShortcut existingShortcut) : this()
{
if (existingShortcut != null)
{
KeyInputTextBox.Text = existingShortcut.Key;
SelectedMailOperation = AvailableMailOperations.FirstOrDefault(x => x.Operation == existingShortcut.MailOperation);
var modifiers = existingShortcut.ModifierKeys;
IsControlPressed = modifiers.HasFlag(ModifierKeys.Control);
IsAltPressed = modifiers.HasFlag(ModifierKeys.Alt);
IsShiftPressed = modifiers.HasFlag(ModifierKeys.Shift);
IsWindowsPressed = modifiers.HasFlag(ModifierKeys.Windows);
Title = "Edit Keyboard Shortcut";
}
}
private void SaveClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
// Clear any previous error
ErrorTextBlock.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
// Validate input
if (string.IsNullOrWhiteSpace(KeyInputTextBox.Text))
{
ShowError("Please enter a key for the shortcut.");
args.Cancel = true;
return;
}
if (SelectedMailOperation == null || SelectedMailOperation.Operation == MailOperation.None)
{
ShowError("Please select a mail operation for the shortcut.");
args.Cancel = true;
return;
}
// Get modifier keys
var modifierKeys = GetSelectedModifierKeys();
// Create successful result
Result = KeyboardShortcutDialogResult.Success(KeyInputTextBox.Text, modifierKeys, SelectedMailOperation.Operation);
}
private void KeyInputTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
// Clear error when user starts typing
ErrorTextBlock.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
var key = e.Key.ToString();
// Update modifier states based on current key press
IsControlPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Control).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down);
IsAltPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Menu).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down);
IsShiftPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Shift).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down);
IsWindowsPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.LeftWindows).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down) ||
Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.RightWindows).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down);
// Set the key (ignore modifier keys themselves)
if (key != "Control" && key != "Menu" && key != "Shift" && key != "LeftWindows" && key != "RightWindows")
{
KeyInputTextBox.Text = key;
}
// Prevent the key from being processed further
// e.Handled = true;
}
private ModifierKeys GetSelectedModifierKeys()
{
var modifiers = ModifierKeys.None;
if (IsControlPressed) modifiers |= ModifierKeys.Control;
if (IsAltPressed) modifiers |= ModifierKeys.Alt;
if (IsShiftPressed) modifiers |= ModifierKeys.Shift;
if (IsWindowsPressed) modifiers |= ModifierKeys.Windows;
return modifiers;
}
private void ShowError(string message)
{
ErrorTextBlock.Text = message;
ErrorTextBlock.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
}
private static List<MailOperationViewModel> GetAvailableMailOperations()
{
var operations = new List<MailOperationViewModel>();
// Add commonly used mail operations that make sense for keyboard shortcuts
var validOperations = new[]
{
MailOperation.Archive,
MailOperation.UnArchive,
MailOperation.SoftDelete,
MailOperation.Move,
MailOperation.MoveToJunk,
MailOperation.SetFlag,
MailOperation.ClearFlag,
MailOperation.MarkAsRead,
MailOperation.MarkAsUnread,
MailOperation.Reply,
MailOperation.ReplyAll,
MailOperation.Forward
};
foreach (var operation in validOperations)
{
operations.Add(new MailOperationViewModel(operation));
}
return operations.OrderBy(x => x.DisplayName).ToList();
}
}
+1 -1
View File
@@ -20,7 +20,7 @@
<Identity
Name="58272BurakKSE.WinoMailPreview"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="0.0.3.0" />
Version="0.0.4.0" />
<mp:PhoneIdentity PhoneProductId="3879fcfb-a561-4599-9103-e0c9b35a271f" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
+15
View File
@@ -10,6 +10,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Synchronization;
@@ -194,4 +195,18 @@ public class DialogService : DialogServiceBase, IMailDialogService
await HandleDialogPresentationAsync(accountReorderDialog);
}
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
public async Task<KeyboardShortcutDialogResult> ShowKeyboardShortcutDialogAsync(KeyboardShortcut existingShortcut = null)
#pragma warning restore CS8625
{
var dialog = new KeyboardShortcutDialog(existingShortcut)
{
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
};
await HandleDialogPresentationAsync(dialog);
return dialog.Result;
}
}
@@ -7,8 +7,8 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.WinUI;
using Wino.Core.WinUI.Services;
using Wino.Core.WinUI.Interfaces;
using Wino.Core.WinUI.Services;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
@@ -59,6 +59,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
WinoPage.LanguageTimePage => typeof(LanguageTimePage),
WinoPage.EditAccountDetailsPage => typeof(EditAccountDetailsPage),
WinoPage.KeyboardShortcutsPage => typeof(KeyboardShortcutsPage),
_ => null,
};
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
using Wino.Core.ViewModels;
using Wino.Views.Abstract;
namespace Wino.Mail.WinUI.Views.Abstract;
public abstract class KeyboardShortcutsPageAbstract : SettingsPageBase<KeyboardShortcutsPageViewModel>
{
}
@@ -0,0 +1,93 @@
<abstract:KeyboardShortcutsPageAbstract
x:Class="Wino.Views.Settings.KeyboardShortcutsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:data="using:Wino.Core.ViewModels.Data"
xmlns:domain="using:Wino.Core.Domain"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="root"
Title="{x:Bind domain:Translator.Settings_KeyboardShortcuts_Title}"
mc:Ignorable="d">
<!-- Main Content -->
<Grid Grid.Row="0">
<!-- Shortcuts List -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="0,0,0,12"
Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{x:Bind ViewModel.StartAddingShortcutCommand}"
Content="{x:Bind domain:Translator.KeyboardShortcuts_Add}" />
<Button
Command="{x:Bind ViewModel.ResetToDefaultsCommand}"
Content="{x:Bind domain:Translator.KeyboardShortcuts_ResetToDefaults}"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<ListView
Grid.Row="1"
ItemsSource="{x:Bind ViewModel.Shortcuts, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="data:KeyboardShortcutViewModel">
<Border
Margin="0,4"
Padding="16,12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock
Margin="0,0,0,4"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind MailOperationDisplayName}" />
<TextBlock
Opacity="0.8"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind DisplayName}" />
</StackPanel>
<Button
Grid.Column="2"
Margin="4,0"
Command="{Binding ViewModel.StartEditingShortcutCommand, ElementName=root}"
CommandParameter="{x:Bind}"
Content="&#xE70F;"
FontFamily="Segoe MDL2 Assets"
Style="{ThemeResource SubtleButtonStyle}"
ToolTipService.ToolTip="Edit" />
<Button
Grid.Column="3"
Margin="4,0"
Command="{Binding ViewModel.DeleteShortcutCommand, ElementName=root}"
CommandParameter="{x:Bind}"
Content="&#xE74D;"
FontFamily="Segoe MDL2 Assets"
Style="{ThemeResource SubtleButtonStyle}"
ToolTipService.ToolTip="Delete" />
</Grid>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</abstract:KeyboardShortcutsPageAbstract>
@@ -0,0 +1,11 @@
using Wino.Mail.WinUI.Views.Abstract;
namespace Wino.Views.Settings;
public sealed partial class KeyboardShortcutsPage : KeyboardShortcutsPageAbstract
{
public KeyboardShortcutsPage()
{
this.InitializeComponent();
}
}
+2 -1
View File
@@ -259,12 +259,13 @@
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningEnabled>False</AppxPackageSigningEnabled>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundle>Always</AppxBundle>
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<PackageCertificateThumbprint>2661584A2F31D3419FFF8AA09A2C16B1991B8290</PackageCertificateThumbprint>
</PropertyGroup>
</Project>
+2 -1
View File
@@ -58,7 +58,8 @@ public class DatabaseService : IDatabaseService
typeof(CalendarEventAttendee),
typeof(CalendarItem),
typeof(Reminder),
typeof(Thumbnail)
typeof(Thumbnail),
typeof(KeyboardShortcut)
);
}
}
+218
View File
@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SqlKata;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Services.Extensions;
namespace Wino.Services;
/// <summary>
/// Service for managing keyboard shortcuts for mail operations.
/// </summary>
public class KeyboardShortcutService : BaseDatabaseService, IKeyboardShortcutService
{
public KeyboardShortcutService(IDatabaseService databaseService) : base(databaseService)
{
}
/// <summary>
/// Gets all available keyboard shortcuts.
/// </summary>
public async Task<IEnumerable<KeyboardShortcut>> GetKeyboardShortcutsAsync()
{
var query = new Query(nameof(KeyboardShortcut))
.OrderBy(nameof(KeyboardShortcut.MailOperation));
return await Connection.QueryAsync<KeyboardShortcut>(query.GetRawQuery());
}
/// <summary>
/// Gets enabled keyboard shortcuts only.
/// </summary>
public async Task<IEnumerable<KeyboardShortcut>> GetEnabledKeyboardShortcutsAsync()
{
var query = new Query(nameof(KeyboardShortcut))
.Where(nameof(KeyboardShortcut.IsEnabled), true)
.OrderBy(nameof(KeyboardShortcut.MailOperation));
return await Connection.QueryAsync<KeyboardShortcut>(query.GetRawQuery());
}
/// <summary>
/// Creates or updates a keyboard shortcut.
/// </summary>
public async Task<KeyboardShortcut> SaveKeyboardShortcutAsync(KeyboardShortcut shortcut)
{
if (shortcut.Id == Guid.Empty)
{
shortcut.Id = Guid.NewGuid();
shortcut.CreatedAt = DateTime.UtcNow;
await Connection.InsertAsync(shortcut);
}
else
{
await Connection.UpdateAsync(shortcut);
}
return shortcut;
}
/// <summary>
/// Deletes a keyboard shortcut.
/// </summary>
public async Task DeleteKeyboardShortcutAsync(Guid shortcutId)
{
var query = new Query(nameof(KeyboardShortcut))
.Where(nameof(KeyboardShortcut.Id), shortcutId);
await Connection.ExecuteAsync($"DELETE FROM {nameof(KeyboardShortcut)} WHERE {nameof(KeyboardShortcut.Id)} = ?", shortcutId);
}
/// <summary>
/// Gets the mail operation for the given key combination.
/// </summary>
public async Task<MailOperation?> GetMailOperationForKeyAsync(string key, ModifierKeys modifierKeys)
{
var query = new Query(nameof(KeyboardShortcut))
.Where(nameof(KeyboardShortcut.Key), key)
.Where(nameof(KeyboardShortcut.ModifierKeys), (int)modifierKeys)
.Where(nameof(KeyboardShortcut.IsEnabled), true);
var shortcut = await Connection.FindWithQueryAsync<KeyboardShortcut>(query.GetRawQuery());
return shortcut?.MailOperation;
}
/// <summary>
/// Checks if a key combination is already assigned to another shortcut.
/// </summary>
public async Task<bool> IsKeyCombinationInUseAsync(string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null)
{
var query = new Query(nameof(KeyboardShortcut))
.Where(nameof(KeyboardShortcut.Key), key)
.Where(nameof(KeyboardShortcut.ModifierKeys), (int)modifierKeys);
if (excludeShortcutId.HasValue)
{
query = query.WhereNot(nameof(KeyboardShortcut.Id), excludeShortcutId.Value);
}
var shortcut = await Connection.FindWithQueryAsync<KeyboardShortcut>(query.GetRawQuery());
return shortcut != null;
}
/// <summary>
/// Creates default keyboard shortcuts for common mail operations.
/// </summary>
public async Task CreateDefaultShortcutsAsync()
{
var defaultShortcuts = GetDefaultShortcuts();
foreach (var shortcut in defaultShortcuts)
{
// Only create if it doesn't exist already
var exists = await IsKeyCombinationInUseAsync(shortcut.Key, shortcut.ModifierKeys);
if (!exists)
{
await SaveKeyboardShortcutAsync(shortcut);
}
}
}
/// <summary>
/// Resets all shortcuts to defaults.
/// </summary>
public async Task ResetToDefaultShortcutsAsync()
{
// Delete all existing shortcuts
await Connection.ExecuteAsync($"DELETE FROM {nameof(KeyboardShortcut)}");
// Create default shortcuts
await CreateDefaultShortcutsAsync();
}
/// <summary>
/// Gets the default keyboard shortcuts.
/// </summary>
private static List<KeyboardShortcut> GetDefaultShortcuts()
{
return new List<KeyboardShortcut>
{
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "Delete",
ModifierKeys = ModifierKeys.None,
MailOperation = MailOperation.SoftDelete,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "Delete",
ModifierKeys = ModifierKeys.Shift,
MailOperation = MailOperation.HardDelete,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "A",
ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.Archive,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "R",
ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.MarkAsRead,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "U",
ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.MarkAsUnread,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "F",
ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.SetFlag,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "F",
ModifierKeys = ModifierKeys.Control | ModifierKeys.Shift,
MailOperation = MailOperation.ClearFlag,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "J",
ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.MoveToJunk,
IsEnabled = true
},
new KeyboardShortcut
{
Id = Guid.NewGuid(),
Key = "M",
ModifierKeys = ModifierKeys.Control,
MailOperation = MailOperation.Move,
IsEnabled = true
}
};
}
}
+1
View File
@@ -23,5 +23,6 @@ public static class ServicesContainerSetup
services.AddTransient<ISignatureService, SignatureService>();
services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>();
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
}
}