mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-02-12 06:41:37 +00:00
Add the possibility to configure the shortcut
This commit is contained in:
parent
6ed9338d23
commit
7dae8c3788
@ -1675,6 +1675,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T20906218
|
||||
-- Use app default
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T3672477670"] = "Use app default"
|
||||
|
||||
-- No shortcut configured
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T3099115336"] = "No shortcut configured"
|
||||
|
||||
-- Configure
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T373171691"] = "Configure"
|
||||
|
||||
-- Configure Keyboard Shortcut
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T636303786"] = "Configure Keyboard Shortcut"
|
||||
|
||||
-- Yes, let the AI decide which data sources are needed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1031370894"] = "Yes, let the AI decide which data sources are needed."
|
||||
|
||||
@ -2017,6 +2026,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"]
|
||||
-- Do you want to show preview features in the app?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Do you want to show preview features in the app?"
|
||||
|
||||
-- Voice recording shortcut
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1278320412"] = "Voice recording shortcut"
|
||||
|
||||
-- How often should we check for app updates?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] = "How often should we check for app updates?"
|
||||
|
||||
@ -2047,6 +2059,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"]
|
||||
-- Select the language for the app.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
|
||||
|
||||
-- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused."
|
||||
|
||||
-- Disable dictation and transcription
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Disable dictation and transcription"
|
||||
|
||||
@ -4612,6 +4627,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832
|
||||
-- Preselect one of your profiles?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Preselect one of your profiles?"
|
||||
|
||||
-- Save
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save"
|
||||
|
||||
-- Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1464973299"] = "Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused."
|
||||
|
||||
-- Press a key combination...
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1468443151"] = "Press a key combination..."
|
||||
|
||||
-- Clear Shortcut
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1807313248"] = "Clear Shortcut"
|
||||
|
||||
-- Invalid shortcut: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T189893682"] = "Invalid shortcut: {0}"
|
||||
|
||||
-- This shortcut conflicts with: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T2633102934"] = "This shortcut conflicts with: {0}"
|
||||
|
||||
-- Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3060573513"] = "Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd)."
|
||||
|
||||
-- Shortcut is valid and available.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3159532525"] = "Shortcut is valid and available."
|
||||
|
||||
-- Supported modifiers: Ctrl/Cmd, Shift, Alt. Example: Ctrl+Shift+R
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3774517957"] = "Supported modifiers: Ctrl/Cmd, Shift, Alt. Example: Ctrl+Shift+R"
|
||||
|
||||
-- Configure Keyboard Shortcut
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T636303786"] = "Configure Keyboard Shortcut"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T900713019"] = "Cancel"
|
||||
|
||||
-- Please enter a value.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T3576780391"] = "Please enter a value."
|
||||
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
@inherits ConfigurationBaseCore
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@this.Icon" Color="@this.IconColor"/>
|
||||
<MudText Typo="Typo.body1" Class="flex-grow-1">
|
||||
@if (string.IsNullOrWhiteSpace(this.Shortcut()))
|
||||
{
|
||||
<span style="color: var(--mud-palette-text-secondary);">@T("No shortcut configured")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">
|
||||
@this.GetDisplayShortcut()
|
||||
</MudChip>
|
||||
}
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Edit"
|
||||
OnClick="@this.OpenDialog"
|
||||
Disabled="@this.IsDisabled">
|
||||
@T("Configure")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@ -0,0 +1,94 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A configuration component for capturing and displaying keyboard shortcuts.
|
||||
/// </summary>
|
||||
public partial class ConfigurationShortcut : ConfigurationBaseCore
|
||||
{
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The current shortcut value.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<string> Shortcut { get; set; } = () => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// An action which is called when the shortcut was changed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<string, Task> ShortcutUpdate { get; set; } = _ => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// The name/identifier of the shortcut (used for conflict detection and registration).
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ShortcutName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The icon to display.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = Icons.Material.Filled.Keyboard;
|
||||
|
||||
/// <summary>
|
||||
/// The color of the icon.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Color IconColor { get; set; } = Color.Default;
|
||||
|
||||
#region Overrides of ConfigurationBase
|
||||
|
||||
protected override bool Stretch => true;
|
||||
|
||||
protected override Variant Variant => Variant.Outlined;
|
||||
|
||||
protected override string Label => this.OptionDescription;
|
||||
|
||||
#endregion
|
||||
|
||||
private string GetDisplayShortcut()
|
||||
{
|
||||
var shortcut = this.Shortcut();
|
||||
if (string.IsNullOrWhiteSpace(shortcut))
|
||||
return string.Empty;
|
||||
|
||||
// Convert internal format to display format
|
||||
return shortcut
|
||||
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
|
||||
.Replace("CommandOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl");
|
||||
}
|
||||
|
||||
private async Task OpenDialog()
|
||||
{
|
||||
var currentShortcut = this.Shortcut();
|
||||
var dialogParameters = new DialogParameters<ShortcutDialog>
|
||||
{
|
||||
{ x => x.InitialShortcut, currentShortcut },
|
||||
{ x => x.ShortcutName, this.ShortcutName },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ShortcutDialog>(
|
||||
this.T("Configure Keyboard Shortcut"),
|
||||
dialogParameters,
|
||||
DialogOptions.FULLSCREEN);
|
||||
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
if (dialogResult.Data is string newShortcut)
|
||||
{
|
||||
await this.ShortcutUpdate(newShortcut);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,5 +33,6 @@
|
||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="@T("Select a transcription provider")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)" Data="@this.GetFilteredTranscriptionProviders()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider = selectedValue)" OptionHelp="@T("Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UseTranscriptionProvider, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationShortcut OptionDescription="@T("Voice recording shortcut")" Shortcut="@(() => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording)" ShortcutUpdate="@this.UpdateVoiceRecordingShortcut" ShortcutName="voice_recording_toggle" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
</ExpansionPanel>
|
||||
@ -35,4 +35,10 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
||||
this.SettingsManager.ConfigurationData.App.LanguagePluginId = pluginId;
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.PLUGINS_RELOADED);
|
||||
}
|
||||
|
||||
private async Task UpdateVoiceRecordingShortcut(string shortcut)
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording = shortcut;
|
||||
await this.RustService.UpdateGlobalShortcut("voice_recording_toggle", shortcut);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.MIME;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
@ -145,6 +144,10 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
// Clean up the recording stream if starting failed:
|
||||
await this.FinalizeRecordingStream();
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
63
app/MindWork AI Studio/Dialogs/ShortcutDialog.razor
Normal file
63
app/MindWork AI Studio/Dialogs/ShortcutDialog.razor
Normal file
@ -0,0 +1,63 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Keyboard" Class="mr-2"/>
|
||||
@T("Configure Keyboard Shortcut")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-3 d-flex align-center justify-center shortcut-capture-area"
|
||||
Style="min-height: 80px; cursor: pointer;"
|
||||
Elevation="2"
|
||||
@onclick="@this.FocusInput">
|
||||
@* Hidden input to capture keyboard events *@
|
||||
<input type="text"
|
||||
@ref="this.hiddenInput"
|
||||
@onkeydown="@this.HandleKeyDown"
|
||||
style="position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none;"
|
||||
autocomplete="off"
|
||||
readonly />
|
||||
@if (string.IsNullOrWhiteSpace(this.currentShortcut))
|
||||
{
|
||||
<MudText Typo="Typo.h6" Color="Color.Secondary">
|
||||
@T("Press a key combination...")
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary">
|
||||
@this.GetDisplayShortcut()
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.validationMessage))
|
||||
{
|
||||
<MudAlert Severity="@this.validationSeverity" Class="mb-3">
|
||||
@this.validationMessage
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-2">
|
||||
@T("Supported modifiers: Ctrl/Cmd, Shift, Alt. Example: Ctrl+Shift+R")
|
||||
</MudText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.ClearShortcut" Variant="Variant.Text" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Clear">
|
||||
@T("Clear Shortcut")
|
||||
</MudButton>
|
||||
<MudSpacer/>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Primary" Disabled="@this.hasValidationError">
|
||||
@T("Save")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
379
app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs
Normal file
379
app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs
Normal file
@ -0,0 +1,379 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// A dialog for capturing and configuring keyboard shortcuts.
|
||||
/// </summary>
|
||||
public partial class ShortcutDialog : MSGComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The initial shortcut value (in internal format, e.g., "CmdOrControl+1").
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string InitialShortcut { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The name/identifier of the shortcut for conflict detection.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ShortcutName { get; set; } = string.Empty;
|
||||
|
||||
private ElementReference hiddenInput;
|
||||
private string currentShortcut = string.Empty;
|
||||
private string validationMessage = string.Empty;
|
||||
private Severity validationSeverity = Severity.Info;
|
||||
private bool hasValidationError;
|
||||
|
||||
// Current key state
|
||||
private bool hasCtrl;
|
||||
private bool hasShift;
|
||||
private bool hasAlt;
|
||||
private bool hasMeta;
|
||||
private string? currentKey;
|
||||
|
||||
private bool isFirstRender = true;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
this.currentShortcut = this.InitialShortcut;
|
||||
this.ParseExistingShortcut();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
// Auto-focus the hidden input when the dialog opens
|
||||
if (this.isFirstRender)
|
||||
{
|
||||
this.isFirstRender = false;
|
||||
await this.hiddenInput.FocusAsync();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void ParseExistingShortcut()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.currentShortcut))
|
||||
return;
|
||||
|
||||
// Parse the existing shortcut to set the state
|
||||
var parts = this.currentShortcut.Split('+');
|
||||
foreach (var part in parts)
|
||||
{
|
||||
switch (part.ToLowerInvariant())
|
||||
{
|
||||
case "cmdorcontrol":
|
||||
case "commandorcontrol":
|
||||
case "ctrl":
|
||||
case "control":
|
||||
case "cmd":
|
||||
case "command":
|
||||
this.hasCtrl = true;
|
||||
break;
|
||||
|
||||
case "shift":
|
||||
this.hasShift = true;
|
||||
break;
|
||||
|
||||
case "alt":
|
||||
this.hasAlt = true;
|
||||
break;
|
||||
|
||||
case "meta":
|
||||
case "super":
|
||||
this.hasMeta = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
this.currentKey = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FocusInput()
|
||||
{
|
||||
// Focus the hidden input to capture keyboard events
|
||||
await this.hiddenInput.FocusAsync();
|
||||
}
|
||||
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
// Ignore pure modifier key presses
|
||||
if (IsModifierKey(e.Code))
|
||||
{
|
||||
this.UpdateModifiers(e);
|
||||
this.currentKey = null;
|
||||
this.UpdateShortcutString();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update modifiers
|
||||
this.UpdateModifiers(e);
|
||||
|
||||
// Get the key
|
||||
this.currentKey = TranslateKeyCode(e.Code);
|
||||
|
||||
// Validate: must have at least one modifier + a key
|
||||
if (!this.hasCtrl && !this.hasShift && !this.hasAlt && !this.hasMeta)
|
||||
{
|
||||
this.validationMessage = T("Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).");
|
||||
this.validationSeverity = Severity.Warning;
|
||||
this.hasValidationError = true;
|
||||
this.StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the shortcut string
|
||||
this.UpdateShortcutString();
|
||||
|
||||
// Validate the shortcut
|
||||
await this.ValidateShortcut();
|
||||
}
|
||||
|
||||
private void UpdateModifiers(KeyboardEventArgs e)
|
||||
{
|
||||
this.hasCtrl = e.CtrlKey || e.MetaKey; // Treat Meta (Cmd on Mac) same as Ctrl for cross-platform
|
||||
this.hasShift = e.ShiftKey;
|
||||
this.hasAlt = e.AltKey;
|
||||
this.hasMeta = e is { MetaKey: true, CtrlKey: false }; // Only set meta if not already using ctrl
|
||||
}
|
||||
|
||||
private void UpdateShortcutString()
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
if (this.hasCtrl)
|
||||
parts.Add("CmdOrControl");
|
||||
|
||||
if (this.hasShift)
|
||||
parts.Add("Shift");
|
||||
|
||||
if (this.hasAlt)
|
||||
parts.Add("Alt");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(this.currentKey))
|
||||
parts.Add(this.currentKey);
|
||||
|
||||
this.currentShortcut = parts.Count > 0 ? string.Join("+", parts) : string.Empty;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ValidateShortcut()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.currentShortcut) || string.IsNullOrWhiteSpace(this.currentKey))
|
||||
{
|
||||
this.validationMessage = string.Empty;
|
||||
this.hasValidationError = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the shortcut is valid by trying to register it with Rust
|
||||
var result = await this.RustService.ValidateShortcut(this.currentShortcut);
|
||||
if (result.IsValid)
|
||||
{
|
||||
if (result.HasConflict)
|
||||
{
|
||||
this.validationMessage = string.Format(T("This shortcut conflicts with: {0}"), result.ConflictDescription);
|
||||
this.validationSeverity = Severity.Warning;
|
||||
this.hasValidationError = false; // Allow saving, but warn
|
||||
}
|
||||
else
|
||||
{
|
||||
this.validationMessage = T("Shortcut is valid and available.");
|
||||
this.validationSeverity = Severity.Success;
|
||||
this.hasValidationError = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.validationMessage = string.Format(T("Invalid shortcut: {0}"), result.ErrorMessage);
|
||||
this.validationSeverity = Severity.Error;
|
||||
this.hasValidationError = true;
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private string GetDisplayShortcut()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.currentShortcut))
|
||||
return string.Empty;
|
||||
|
||||
// Convert internal format to display format
|
||||
return this.currentShortcut
|
||||
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
|
||||
.Replace("CommandOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl");
|
||||
}
|
||||
|
||||
private void ClearShortcut()
|
||||
{
|
||||
this.currentShortcut = string.Empty;
|
||||
this.currentKey = null;
|
||||
this.hasCtrl = false;
|
||||
this.hasShift = false;
|
||||
this.hasAlt = false;
|
||||
this.hasMeta = false;
|
||||
this.validationMessage = string.Empty;
|
||||
this.hasValidationError = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.currentShortcut));
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the key code represents a modifier key.
|
||||
/// </summary>
|
||||
private static bool IsModifierKey(string code) => code switch
|
||||
{
|
||||
"ShiftLeft" or "ShiftRight" => true,
|
||||
"ControlLeft" or "ControlRight" => true,
|
||||
"AltLeft" or "AltRight" => true,
|
||||
"MetaLeft" or "MetaRight" => true,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Translates a JavaScript KeyboardEvent.code to Tauri shortcut format.
|
||||
/// </summary>
|
||||
private static string TranslateKeyCode(string code) => code switch
|
||||
{
|
||||
// Letters
|
||||
"KeyA" => "A",
|
||||
"KeyB" => "B",
|
||||
"KeyC" => "C",
|
||||
"KeyD" => "D",
|
||||
"KeyE" => "E",
|
||||
"KeyF" => "F",
|
||||
"KeyG" => "G",
|
||||
"KeyH" => "H",
|
||||
"KeyI" => "I",
|
||||
"KeyJ" => "J",
|
||||
"KeyK" => "K",
|
||||
"KeyL" => "L",
|
||||
"KeyM" => "M",
|
||||
"KeyN" => "N",
|
||||
"KeyO" => "O",
|
||||
"KeyP" => "P",
|
||||
"KeyQ" => "Q",
|
||||
"KeyR" => "R",
|
||||
"KeyS" => "S",
|
||||
"KeyT" => "T",
|
||||
"KeyU" => "U",
|
||||
"KeyV" => "V",
|
||||
"KeyW" => "W",
|
||||
"KeyX" => "X",
|
||||
"KeyY" => "Y",
|
||||
"KeyZ" => "Z",
|
||||
|
||||
// Numbers
|
||||
"Digit0" => "0",
|
||||
"Digit1" => "1",
|
||||
"Digit2" => "2",
|
||||
"Digit3" => "3",
|
||||
"Digit4" => "4",
|
||||
"Digit5" => "5",
|
||||
"Digit6" => "6",
|
||||
"Digit7" => "7",
|
||||
"Digit8" => "8",
|
||||
"Digit9" => "9",
|
||||
|
||||
// Function keys
|
||||
"F1" => "F1",
|
||||
"F2" => "F2",
|
||||
"F3" => "F3",
|
||||
"F4" => "F4",
|
||||
"F5" => "F5",
|
||||
"F6" => "F6",
|
||||
"F7" => "F7",
|
||||
"F8" => "F8",
|
||||
"F9" => "F9",
|
||||
"F10" => "F10",
|
||||
"F11" => "F11",
|
||||
"F12" => "F12",
|
||||
"F13" => "F13",
|
||||
"F14" => "F14",
|
||||
"F15" => "F15",
|
||||
"F16" => "F16",
|
||||
"F17" => "F17",
|
||||
"F18" => "F18",
|
||||
"F19" => "F19",
|
||||
"F20" => "F20",
|
||||
"F21" => "F21",
|
||||
"F22" => "F22",
|
||||
"F23" => "F23",
|
||||
"F24" => "F24",
|
||||
|
||||
// Special keys
|
||||
"Space" => "Space",
|
||||
"Enter" => "Enter",
|
||||
"Tab" => "Tab",
|
||||
"Escape" => "Escape",
|
||||
"Backspace" => "Backspace",
|
||||
"Delete" => "Delete",
|
||||
"Insert" => "Insert",
|
||||
"Home" => "Home",
|
||||
"End" => "End",
|
||||
"PageUp" => "PageUp",
|
||||
"PageDown" => "PageDown",
|
||||
|
||||
// Arrow keys
|
||||
"ArrowUp" => "Up",
|
||||
"ArrowDown" => "Down",
|
||||
"ArrowLeft" => "Left",
|
||||
"ArrowRight" => "Right",
|
||||
|
||||
// Numpad
|
||||
"Numpad0" => "Num0",
|
||||
"Numpad1" => "Num1",
|
||||
"Numpad2" => "Num2",
|
||||
"Numpad3" => "Num3",
|
||||
"Numpad4" => "Num4",
|
||||
"Numpad5" => "Num5",
|
||||
"Numpad6" => "Num6",
|
||||
"Numpad7" => "Num7",
|
||||
"Numpad8" => "Num8",
|
||||
"Numpad9" => "Num9",
|
||||
"NumpadAdd" => "NumAdd",
|
||||
"NumpadSubtract" => "NumSubtract",
|
||||
"NumpadMultiply" => "NumMultiply",
|
||||
"NumpadDivide" => "NumDivide",
|
||||
"NumpadDecimal" => "NumDecimal",
|
||||
"NumpadEnter" => "NumEnter",
|
||||
|
||||
// Punctuation
|
||||
"Minus" => "Minus",
|
||||
"Equal" => "Equal",
|
||||
"BracketLeft" => "BracketLeft",
|
||||
"BracketRight" => "BracketRight",
|
||||
"Backslash" => "Backslash",
|
||||
"Semicolon" => "Semicolon",
|
||||
"Quote" => "Quote",
|
||||
"Backquote" => "Backquote",
|
||||
"Comma" => "Comma",
|
||||
"Period" => "Period",
|
||||
"Slash" => "Slash",
|
||||
|
||||
// Default: return as-is
|
||||
_ => code,
|
||||
};
|
||||
}
|
||||
@ -113,9 +113,30 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
await this.UpdateThemeConfiguration();
|
||||
this.LoadNavItems();
|
||||
|
||||
await this.RegisterGlobalShortcuts();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers global shortcuts based on the current configuration.
|
||||
/// </summary>
|
||||
private async Task RegisterGlobalShortcuts()
|
||||
{
|
||||
// Only register the voice recording shortcut if the preview feature is enabled:
|
||||
if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
var shortcut = this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording;
|
||||
if (!string.IsNullOrWhiteSpace(shortcut))
|
||||
{
|
||||
var success = await this.RustService.UpdateGlobalShortcut("voice_recording_toggle", shortcut);
|
||||
if (success)
|
||||
this.Logger.LogInformation("Global voice recording shortcut '{Shortcut}' registered successfully.", shortcut);
|
||||
else
|
||||
this.Logger.LogWarning("Failed to register global voice recording shortcut '{Shortcut}'.", shortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadNavItems()
|
||||
{
|
||||
this.navItems = new List<NavBarItem>(this.GetNavItems());
|
||||
|
||||
@ -82,6 +82,13 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
||||
/// </summary>
|
||||
public string UseTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.UseTranscriptionProvider, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The global keyboard shortcut for toggling voice recording.
|
||||
/// Uses Tauri's shortcut format, e.g., "CmdOrControl+1" (Cmd+1 on macOS, Ctrl+1 on Windows/Linux).
|
||||
/// Set to empty string to disable the global shortcut.
|
||||
/// </summary>
|
||||
public string ShortcutVoiceRecording { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShortcutVoiceRecording, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Should the user be allowed to add providers?
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers or updates a global keyboard shortcut.
|
||||
/// </summary>
|
||||
/// <param name="name">The name/identifier for the shortcut (e.g., "voice_recording_toggle").</param>
|
||||
/// <param name="shortcut">The shortcut string in Tauri format (e.g., "CmdOrControl+1"). Use empty string to disable.</param>
|
||||
/// <returns>True if the shortcut was registered successfully, false otherwise.</returns>
|
||||
public async Task<bool> UpdateGlobalShortcut(string name, string shortcut)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new RegisterShortcutRequest(name, shortcut);
|
||||
var response = await this.http.PostAsJsonAsync("/shortcuts/register", request, this.jsonRustSerializerOptions);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger?.LogError("Failed to register global shortcut '{Name}' due to network error: {StatusCode}", name, response.StatusCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<ShortcutResponse>(this.jsonRustSerializerOptions);
|
||||
if (result is null || !result.Success)
|
||||
{
|
||||
this.logger?.LogError("Failed to register global shortcut '{Name}': {Error}", name, result?.ErrorMessage ?? "Unknown error");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger?.LogInformation("Global shortcut '{Name}' registered successfully with key '{Shortcut}'.", name, shortcut);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger?.LogError(ex, "Exception while registering global shortcut '{Name}'.", name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a shortcut string without registering it.
|
||||
/// </summary>
|
||||
/// <param name="shortcut">The shortcut string to validate.</param>
|
||||
/// <returns>A validation result indicating if the shortcut is valid and any conflicts.</returns>
|
||||
public async Task<ShortcutValidationResult> ValidateShortcut(string shortcut)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new ValidateShortcutRequest(shortcut);
|
||||
var response = await this.http.PostAsJsonAsync("/shortcuts/validate", request, this.jsonRustSerializerOptions);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger?.LogError("Failed to validate shortcut due to network error: {StatusCode}", response.StatusCode);
|
||||
return new ShortcutValidationResult(false, "Network error during validation", false, string.Empty);
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<ShortcutValidationResponse>(this.jsonRustSerializerOptions);
|
||||
if (result is null)
|
||||
return new ShortcutValidationResult(false, "Invalid response from server", false, string.Empty);
|
||||
|
||||
return new ShortcutValidationResult(result.IsValid, result.ErrorMessage, result.HasConflict, result.ConflictDescription);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger?.LogError(ex, "Exception while validating shortcut.");
|
||||
return new ShortcutValidationResult(false, ex.Message, false, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record RegisterShortcutRequest(string Name, string Shortcut);
|
||||
|
||||
private sealed record ShortcutResponse(bool Success, string ErrorMessage);
|
||||
|
||||
private sealed record ValidateShortcutRequest(string Shortcut);
|
||||
|
||||
private sealed record ShortcutValidationResponse(bool IsValid, string ErrorMessage, bool HasConflict, string ConflictDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of validating a keyboard shortcut.
|
||||
/// </summary>
|
||||
/// <param name="IsValid">Whether the shortcut syntax is valid.</param>
|
||||
/// <param name="ErrorMessage">Error message if not valid.</param>
|
||||
/// <param name="HasConflict">Whether the shortcut conflicts with another registered shortcut.</param>
|
||||
/// <param name="ConflictDescription">Description of the conflict if any.</param>
|
||||
public sealed record ShortcutValidationResult(bool IsValid, string ErrorMessage, bool HasConflict, string ConflictDescription);
|
||||
@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
@ -27,6 +28,9 @@ static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<UpdateResponse<tauri::Wry>>>> =
|
||||
/// The event broadcast sender for Tauri events.
|
||||
static EVENT_BROADCAST: Lazy<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
/// Stores the currently registered global shortcuts (name -> shortcut string).
|
||||
static REGISTERED_SHORTCUTS: Lazy<Mutex<HashMap<String, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// Starts the Tauri app.
|
||||
pub fn start_tauri() {
|
||||
info!("Starting Tauri app...");
|
||||
@ -65,9 +69,6 @@ pub fn start_tauri() {
|
||||
// Get the main window:
|
||||
let window = app.get_window("main").expect("Failed to get main window.");
|
||||
|
||||
// Clone the event sender for the global shortcut handler before moving it into the window event closure:
|
||||
let shortcut_event_sender = event_sender.clone();
|
||||
|
||||
// Register a callback for window events, such as file drops. We have to use
|
||||
// this handler in addition to the app event handler, because file drop events
|
||||
// are only available in the window event handler (is a bug, cf. https://github.com/tauri-apps/tauri/issues/14338):
|
||||
@ -86,24 +87,6 @@ pub fn start_tauri() {
|
||||
// Save the main window for later access:
|
||||
*MAIN_WINDOW.lock().unwrap() = Some(window);
|
||||
|
||||
// Register global shortcuts for voice recording toggle.
|
||||
// CmdOrControl+1 will be Cmd+1 on macOS and Ctrl+1 on Windows/Linux:
|
||||
let mut shortcut_manager = app.global_shortcut_manager();
|
||||
match shortcut_manager.register("CmdOrControl+1", move || {
|
||||
info!(Source = "Tauri"; "Global shortcut CmdOrControl+1 triggered for voice recording toggle.");
|
||||
let event = Event::new(TauriEventType::GlobalShortcutPressed, vec!["voice_recording_toggle".to_string()]);
|
||||
let sender = shortcut_event_sender.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match sender.send(event) {
|
||||
Ok(_) => {},
|
||||
Err(error) => error!(Source = "Tauri"; "Failed to send global shortcut event: {error}"),
|
||||
}
|
||||
});
|
||||
}) {
|
||||
Ok(_) => info!(Source = "Bootloader Tauri"; "Global shortcut CmdOrControl+1 registered successfully."),
|
||||
Err(error) => error!(Source = "Bootloader Tauri"; "Failed to register global shortcut: {error}"),
|
||||
}
|
||||
|
||||
info!(Source = "Bootloader Tauri"; "Setup is running.");
|
||||
let data_path = app.path_resolver().app_local_data_dir().unwrap();
|
||||
let data_path = data_path.join("data");
|
||||
@ -706,6 +689,232 @@ pub struct FileSaveResponse {
|
||||
save_file_path: String,
|
||||
}
|
||||
|
||||
/// Request payload for registering a global shortcut.
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct RegisterShortcutRequest {
|
||||
/// The name/identifier for the shortcut (e.g., "voice_recording_toggle").
|
||||
name: String,
|
||||
|
||||
/// The shortcut string in Tauri format (e.g., "CmdOrControl+1").
|
||||
/// Use empty string to unregister the shortcut.
|
||||
shortcut: String,
|
||||
}
|
||||
|
||||
/// Response for shortcut registration.
|
||||
#[derive(Serialize)]
|
||||
pub struct ShortcutResponse {
|
||||
success: bool,
|
||||
error_message: String,
|
||||
}
|
||||
|
||||
/// Registers or updates a global shortcut. If the shortcut string is empty,
|
||||
/// the existing shortcut for that name will be unregistered.
|
||||
#[post("/shortcuts/register", data = "<payload>")]
|
||||
pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest>) -> Json<ShortcutResponse> {
|
||||
let name = payload.name.clone();
|
||||
let new_shortcut = payload.shortcut.clone();
|
||||
|
||||
info!(Source = "Tauri"; "Registering global shortcut '{name}' with key '{new_shortcut}'.");
|
||||
|
||||
// Get the main window to access the global shortcut manager:
|
||||
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
||||
let main_window = match main_window_lock.as_ref() {
|
||||
Some(window) => window,
|
||||
None => {
|
||||
error!(Source = "Tauri"; "Cannot register shortcut: main window not available.");
|
||||
return Json(ShortcutResponse {
|
||||
success: false,
|
||||
error_message: "Main window not available".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mut shortcut_manager = main_window.app_handle().global_shortcut_manager();
|
||||
let mut registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
||||
|
||||
// Unregister the old shortcut if one exists for this name:
|
||||
if let Some(old_shortcut) = registered_shortcuts.get(&name) {
|
||||
if !old_shortcut.is_empty() {
|
||||
match shortcut_manager.unregister(old_shortcut.as_str()) {
|
||||
Ok(_) => info!(Source = "Tauri"; "Unregistered old shortcut '{old_shortcut}' for '{name}'."),
|
||||
Err(error) => warn!(Source = "Tauri"; "Failed to unregister old shortcut '{old_shortcut}': {error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the new shortcut is empty, we're done (just unregistering):
|
||||
if new_shortcut.is_empty() {
|
||||
registered_shortcuts.remove(&name);
|
||||
info!(Source = "Tauri"; "Shortcut '{name}' has been disabled.");
|
||||
return Json(ShortcutResponse {
|
||||
success: true,
|
||||
error_message: String::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// Get the event broadcast sender for the shortcut callback:
|
||||
let event_broadcast_lock = EVENT_BROADCAST.lock().unwrap();
|
||||
let event_sender = match event_broadcast_lock.as_ref() {
|
||||
Some(sender) => sender.clone(),
|
||||
None => {
|
||||
error!(Source = "Tauri"; "Cannot register shortcut: event broadcast not initialized.");
|
||||
return Json(ShortcutResponse {
|
||||
success: false,
|
||||
error_message: "Event broadcast not initialized".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
drop(event_broadcast_lock);
|
||||
|
||||
// Register the new shortcut:
|
||||
let shortcut_name = name.clone();
|
||||
match shortcut_manager.register(new_shortcut.as_str(), move || {
|
||||
info!(Source = "Tauri"; "Global shortcut triggered for '{shortcut_name}'.");
|
||||
let event = Event::new(TauriEventType::GlobalShortcutPressed, vec![shortcut_name.clone()]);
|
||||
let sender = event_sender.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match sender.send(event) {
|
||||
Ok(_) => {},
|
||||
Err(error) => error!(Source = "Tauri"; "Failed to send global shortcut event: {error}"),
|
||||
}
|
||||
});
|
||||
})
|
||||
{
|
||||
Ok(_) => {
|
||||
info!(Source = "Tauri"; "Global shortcut '{new_shortcut}' registered successfully for '{name}'.");
|
||||
registered_shortcuts.insert(name, new_shortcut);
|
||||
Json(ShortcutResponse {
|
||||
success: true,
|
||||
error_message: String::new(),
|
||||
})
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
let error_msg = format!("Failed to register shortcut: {error}");
|
||||
error!(Source = "Tauri"; "{error_msg}");
|
||||
Json(ShortcutResponse {
|
||||
success: false,
|
||||
error_message: error_msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request payload for validating a shortcut.
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct ValidateShortcutRequest {
|
||||
/// The shortcut string to validate (e.g., "CmdOrControl+1").
|
||||
shortcut: String,
|
||||
}
|
||||
|
||||
/// Response for shortcut validation.
|
||||
#[derive(Serialize)]
|
||||
pub struct ShortcutValidationResponse {
|
||||
is_valid: bool,
|
||||
error_message: String,
|
||||
has_conflict: bool,
|
||||
conflict_description: String,
|
||||
}
|
||||
|
||||
/// Validates a shortcut string without registering it.
|
||||
/// Checks if the shortcut syntax is valid and if it
|
||||
/// conflicts with existing shortcuts.
|
||||
#[post("/shortcuts/validate", data = "<payload>")]
|
||||
pub fn validate_shortcut(_token: APIToken, payload: Json<ValidateShortcutRequest>) -> Json<ShortcutValidationResponse> {
|
||||
let shortcut = payload.shortcut.clone();
|
||||
|
||||
// Empty shortcuts are always valid (means "disabled"):
|
||||
if shortcut.is_empty() {
|
||||
return Json(ShortcutValidationResponse {
|
||||
is_valid: true,
|
||||
error_message: String::new(),
|
||||
has_conflict: false,
|
||||
conflict_description: String::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the shortcut is already registered:
|
||||
let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap();
|
||||
for (name, registered_shortcut) in registered_shortcuts.iter() {
|
||||
if registered_shortcut.eq_ignore_ascii_case(&shortcut) {
|
||||
return Json(ShortcutValidationResponse {
|
||||
is_valid: true,
|
||||
error_message: String::new(),
|
||||
has_conflict: true,
|
||||
conflict_description: format!("Already used by: {}", name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
drop(registered_shortcuts);
|
||||
|
||||
// Try to parse the shortcut to validate syntax.
|
||||
// We can't easily validate without registering in Tauri 1.x,
|
||||
// so we do basic syntax validation here:
|
||||
let is_valid = validate_shortcut_syntax(&shortcut);
|
||||
|
||||
if is_valid {
|
||||
Json(ShortcutValidationResponse {
|
||||
is_valid: true,
|
||||
error_message: String::new(),
|
||||
has_conflict: false,
|
||||
conflict_description: String::new(),
|
||||
})
|
||||
} else {
|
||||
Json(ShortcutValidationResponse {
|
||||
is_valid: false,
|
||||
error_message: format!("Invalid shortcut syntax: {}", shortcut),
|
||||
has_conflict: false,
|
||||
conflict_description: String::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the syntax of a shortcut string.
|
||||
fn validate_shortcut_syntax(shortcut: &str) -> bool {
|
||||
let parts: Vec<&str> = shortcut.split('+').collect();
|
||||
if parts.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut has_key = false;
|
||||
for part in parts {
|
||||
let part_lower = part.to_lowercase();
|
||||
match part_lower.as_str() {
|
||||
// Modifiers
|
||||
"cmdorcontrol" | "commandorcontrol" | "ctrl" | "control" | "cmd" | "command" |
|
||||
"shift" | "alt" | "meta" | "super" | "option" => continue,
|
||||
|
||||
// Keys - letters
|
||||
"a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" |
|
||||
"n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" => has_key = true,
|
||||
|
||||
// Keys - numbers
|
||||
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => has_key = true,
|
||||
|
||||
// Keys - function keys
|
||||
_ if part_lower.starts_with('f') && part_lower[1..].parse::<u32>().is_ok() => has_key = true,
|
||||
|
||||
// Keys - special
|
||||
"space" | "enter" | "tab" | "escape" | "backspace" | "delete" | "insert" |
|
||||
"home" | "end" | "pageup" | "pagedown" |
|
||||
"up" | "down" | "left" | "right" |
|
||||
"arrowup" | "arrowdown" | "arrowleft" | "arrowright" |
|
||||
"minus" | "equal" | "bracketleft" | "bracketright" | "backslash" |
|
||||
"semicolon" | "quote" | "backquote" | "comma" | "period" | "slash" => has_key = true,
|
||||
|
||||
// Keys - numpad
|
||||
_ if part_lower.starts_with("num") => has_key = true,
|
||||
|
||||
// Unknown
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
|
||||
has_key
|
||||
}
|
||||
|
||||
fn set_pdfium_path(path_resolver: PathResolver) {
|
||||
let pdfium_relative_source_path = String::from("resources/libraries/");
|
||||
let pdfium_source_path = path_resolver.resolve_resource(pdfium_relative_source_path);
|
||||
|
||||
@ -87,6 +87,8 @@ pub fn start_runtime_api() {
|
||||
crate::file_data::extract_data,
|
||||
crate::log::get_log_paths,
|
||||
crate::log::log_event,
|
||||
crate::app_window::register_shortcut,
|
||||
crate::app_window::validate_shortcut,
|
||||
])
|
||||
.ignite().await.unwrap()
|
||||
.launch().await.unwrap();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user