Added focused-window shortcut fallback on Linux (#882)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-07-21 14:43:33 +02:00
1 parent db508edc28
commit 1e5f07cb01
11 files changed
+350 -17

No files matched your search

@@ -15,7 +15,7 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
private IDialogService DialogService { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
private GlobalShortcutService GlobalShortcutService { get; init; } = null!;
/// <summary>
/// The shortcut binding data.
@@ -69,7 +69,7 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
{
// Suspend shortcut processing while the dialog is open, so the user can
// press the current shortcut to re-enter it without triggering the action:
await this.RustService.SuspendShortcutProcessing();
await this.GlobalShortcutService.SuspendShortcutProcessing();
try
{
@@ -106,7 +106,7 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
finally
{
// Resume the shortcut processing when the dialog is closed:
await this.RustService.ResumeShortcutProcessing();
await this.GlobalShortcutService.ResumeShortcutProcessing();
}
}
}
@@ -22,6 +22,9 @@ public partial class VoiceRecorder : MSGComponentBase
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private GlobalShortcutService GlobalShortcutService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
@@ -35,6 +38,8 @@ public partial class VoiceRecorder : MSGComponentBase
protected override async Task OnInitializedAsync()
{
this.GlobalShortcutService.RuntimeStateChanged += this.OnShortcutRuntimeStateChanged;
// Register for global shortcut events:
this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED, Event.VOICE_RECORDING_AVAILABILITY_CHANGED]);
@@ -43,8 +48,15 @@ public partial class VoiceRecorder : MSGComponentBase
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && this.ShouldRenderVoiceRecording)
await this.EnsureSoundEffectsAvailableAsync("during the first interactive render");
if (firstRender)
{
this.localShortcutDotNetReference = DotNetObjectReference.Create(this);
this.localShortcutInteropReady = true;
await this.ApplyLocalShortcutState(this.GlobalShortcutService.GetRuntimeState(Shortcut.VOICE_RECORDING_TOGGLE));
if (this.ShouldRenderVoiceRecording)
await this.EnsureSoundEffectsAvailableAsync("during the first interactive render");
}
await base.OnAfterRenderAsync(firstRender);
}
@@ -69,6 +81,36 @@ public partial class VoiceRecorder : MSGComponentBase
}
}
private async Task OnShortcutRuntimeStateChanged(GlobalShortcutRuntimeState runtimeState)
{
try
{
await this.InvokeAsync(() => this.ApplyLocalShortcutState(runtimeState));
}
catch (ObjectDisposedException)
{
this.Logger.LogDebug("Ignoring a shortcut state change after the voice recorder was disposed.");
}
catch (InvalidOperationException ex)
{
this.Logger.LogDebug(ex, "The focused-window shortcut listener could not be updated because the component dispatcher is unavailable.");
}
}
[JSInvokable]
public async Task OnLocalShortcutPressed()
{
var runtimeState = this.GlobalShortcutService.GetRuntimeState(Shortcut.VOICE_RECORDING_TOGGLE);
if (runtimeState.Backend is not ShortcutBackend.LOCAL || runtimeState.IsSuspended)
{
this.Logger.LogDebug("Ignoring a stale focused-window shortcut event.");
return;
}
this.Logger.LogInformation("Focused-window shortcut triggered for voice recording toggle.");
await this.ToggleRecordingFromShortcut();
}
/// <summary>
/// Toggles the recording state when triggered by a global shortcut.
/// </summary>
@@ -101,6 +143,48 @@ public partial class VoiceRecorder : MSGComponentBase
private string? currentRecordingPath;
private string? finalRecordingPath;
private DotNetObjectReference<VoiceRecorder>? dotNetReference;
private DotNetObjectReference<VoiceRecorder>? localShortcutDotNetReference;
private bool localShortcutInteropReady;
private async Task ApplyLocalShortcutState(GlobalShortcutRuntimeState runtimeState)
{
if (!this.localShortcutInteropReady
|| this.localShortcutDotNetReference is null
|| runtimeState.ShortcutId is not Shortcut.VOICE_RECORDING_TOGGLE)
{
return;
}
try
{
if (runtimeState.Backend is ShortcutBackend.LOCAL
&& !runtimeState.IsSuspended
&& !string.IsNullOrWhiteSpace(runtimeState.Shortcut))
{
await this.JsRuntime.InvokeVoidAsync(
"localShortcut.register",
"voice-recording-toggle",
runtimeState.Shortcut,
this.localShortcutDotNetReference);
}
else
{
await this.JsRuntime.InvokeVoidAsync("localShortcut.unregister", "voice-recording-toggle");
}
}
catch (JSDisconnectedException)
{
this.Logger.LogDebug("The focused-window shortcut listener could not be updated because the JS runtime disconnected.");
}
catch (OperationCanceledException)
{
this.Logger.LogDebug("Updating the focused-window shortcut listener was canceled.");
}
catch (JSException ex)
{
this.Logger.LogWarning(ex, "Failed to update the focused-window shortcut listener.");
}
}
private bool ShouldRenderVoiceRecording => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)
&& !string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider);
@@ -482,6 +566,15 @@ public partial class VoiceRecorder : MSGComponentBase
protected override void DisposeResources()
{
this.GlobalShortcutService.RuntimeStateChanged -= this.OnShortcutRuntimeStateChanged;
if (this.localShortcutInteropReady)
_ = this.JsRuntime.InvokeVoidAsync("localShortcut.unregister", "voice-recording-toggle");
this.localShortcutDotNetReference?.Dispose();
this.localShortcutDotNetReference = null;
this.localShortcutInteropReady = false;
// Clean up recording resources if still active:
if (this.currentRecordingStream is not null)
{