diff --git a/src/main/events.ts b/src/main/events.ts index f331a53..6932d68 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -13,3 +13,5 @@ export const AppEvents = new EventEmitter<{ userAssetChanged: [UserAssetType]; setTrayVariant: ["tray" | "trayUnread"]; }>(); + +export const appReady = new Promise(resolve => AppEvents.once("appLoaded", () => resolve())); diff --git a/src/main/index.ts b/src/main/index.ts index cae0fcc..1458fc3 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -167,5 +167,5 @@ app.on("second-instance", (_e, _argv, _cwd, data) => { const { args } = data as { args: typeof CommandLine; IS_DEV: boolean }; const { "run-shortcut": shortcut } = args.values; - if (shortcut) sendRendererCommand(IpcCommands.HANDLE_KEY_BIND, shortcut); + if (shortcut) sendRendererCommand(IpcCommands.KEY_BINDS_HANDLE, shortcut); }); diff --git a/src/main/ipcCommands.ts b/src/main/ipcCommands.ts index 8731b8d..b42e032 100644 --- a/src/main/ipcCommands.ts +++ b/src/main/ipcCommands.ts @@ -8,6 +8,7 @@ import { randomUUID } from "crypto"; import { ipcMain } from "electron"; import { IpcEvents } from "shared/IpcEvents"; +import { appReady } from "./events"; import { mainWin } from "./mainWindow"; const resolvers = new Map void>>(); @@ -30,7 +31,9 @@ export interface IpcResponse { * * You must add a handler for the message in the renderer process. */ -export function sendRendererCommand(message: string, data?: any) { +export async function sendRendererCommand(message: string, data?: any) { + await appReady; + if (mainWin.isDestroyed()) { console.warn("Main window is destroyed, cannot send IPC command:", message); return Promise.reject(new Error("Main window is destroyed")); diff --git a/src/main/keyBinds.ts b/src/main/keyBinds.ts index 0624030..5bcbf43 100644 --- a/src/main/keyBinds.ts +++ b/src/main/keyBinds.ts @@ -12,7 +12,6 @@ import { Settings } from "./settings"; function unregisterKeyBinds() { if (!app.isReady()) return; - globalShortcut.unregisterAll(); } @@ -21,15 +20,18 @@ export async function registerKeyBinds() { unregisterKeyBinds(); + let success = true; + for (const { action, enabled, key } of Settings.store.keyBinds || []) { if (!enabled || !key || action === "unassigned") continue; - console.log( - key, - "SUCCESS", - globalShortcut.register(key, () => sendRendererCommand(IpcCommands.HANDLE_KEY_BIND, action)) + const registeredSuccessfully = globalShortcut.register(key, () => + sendRendererCommand(IpcCommands.KEY_BINDS_HANDLE, action) ); + success &&= registeredSuccessfully; } + + sendRendererCommand(IpcCommands.KEY_BINDS_SET_STATUS, success); } Settings.addChangeListener("keyBinds", registerKeyBinds); diff --git a/src/renderer/components/settings/Settings.tsx b/src/renderer/components/settings/Settings.tsx index b3be64a..0313938 100644 --- a/src/renderer/components/settings/Settings.tsx +++ b/src/renderer/components/settings/Settings.tsx @@ -9,7 +9,7 @@ import "./settings.css"; import { classNameFactory } from "@vencord/types/api/Styles"; import { BaseText, Divider, ErrorBoundary } from "@vencord/types/components"; import { ComponentType } from "react"; -import { KeyBindSettings } from "renderer/globalShortcuts/ShortcutSettings"; +import { KeybindsButton } from "renderer/globalShortcuts/ShortcutSettings"; import { Settings, useSettings } from "renderer/settings"; import { isMac, isWindows } from "renderer/utils"; @@ -123,7 +123,8 @@ const SettingsOptions: Record> title: "Disable smooth scrolling", description: "Disables smooth scrolling", defaultValue: false - } + }, + KeybindsButton ], Notifications: [ NotificationBadgeToggle, @@ -149,7 +150,6 @@ const SettingsOptions: Record> defaultValue: false } ], - KeyBinds: [KeyBindSettings], "Developer Options": [DeveloperOptionsButton] }; diff --git a/src/renderer/globalShortcuts/ShortcutSettings.css b/src/renderer/globalShortcuts/ShortcutSettings.css index fdbfd24..8b7b689 100644 --- a/src/renderer/globalShortcuts/ShortcutSettings.css +++ b/src/renderer/globalShortcuts/ShortcutSettings.css @@ -1,6 +1,13 @@ +.vcd-shortcuts-container { + display: flex; + flex-direction: column; + gap: 1em; + padding-block: 1em; +} + .vcd-shortcuts-group { display: grid; - gap: 0.25em 1em; + gap: 0.25em 0.5em; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto auto; align-items: center; } @@ -21,7 +28,8 @@ border-radius: var(--radius-sm); width: 100%; min-height: calc(var(--select-option-height) - 2px); - padding: 0.25em; + padding: 3px 4px 3px 12px; + box-sizing: border-box; } .vcd-shortcuts-record-input { diff --git a/src/renderer/globalShortcuts/ShortcutSettings.tsx b/src/renderer/globalShortcuts/ShortcutSettings.tsx index 378598d..e35c8f3 100644 --- a/src/renderer/globalShortcuts/ShortcutSettings.tsx +++ b/src/renderer/globalShortcuts/ShortcutSettings.tsx @@ -6,10 +6,23 @@ import "./ShortcutSettings.css"; -import { BaseText, Button, DeleteIcon, Switch } from "@vencord/types/components"; -import { classNameFactory, identity } from "@vencord/types/utils"; +import { BaseText, Button, Card, DeleteIcon, Switch } from "@vencord/types/components"; +import { + classNameFactory, + identity, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalRoot, + ModalSize, + openModal +} from "@vencord/types/utils"; import { React, Select, useState } from "@vencord/types/webpack/common"; +import { SettingsComponent } from "renderer/components/settings/Settings"; +import { onIpcCommand } from "renderer/ipcCommands"; +import { reactiveValue } from "renderer/reactiveState"; import { Settings } from "renderer/settings"; +import { IpcCommands } from "shared/IpcEvents"; import { recordKeybind } from "./recordKeybind"; @@ -20,13 +33,14 @@ const ShortcutActions = { toggleMute: "Toggle Mute", toggleDeafen: "Toggle Deafen", toggleStreamerMode: "Toggle Streamer Mode", - toggleCamera: "Toggle Camera", - toggleScreenShare: "Toggle Screen Share", - disconnectFromVoiceChannel: "Disconnect from Voice Channel" + disconnectFromVoiceChannel: "Disconnect from VC" } as const; export type ShortcutAction = keyof typeof ShortcutActions; +const shortcutStatus = reactiveValue(true); +onIpcCommand(IpcCommands.KEY_BINDS_SET_STATUS, status => (shortcutStatus.value = status)); + export interface KeyBind { id: string; enabled: boolean; @@ -109,9 +123,18 @@ function KeyBindGroup({ export function KeyBindSettings() { const [keyBinds, setKeyBinds] = useState(Settings.store.keyBinds ?? []); + const shortcutsStatus = shortcutStatus.use(); return ( -
+
+ {shortcutsStatus === false && ( + + + Some of your keybinds failed to register. + + + )} + {keyBinds.map((keyBind, index) => ( ); } + +export function openKeybindsModal() { + openModal(props => ( + + + + Keybinds + + + + + + + + + )); +} + +export const KeybindsButton: SettingsComponent = () => { + return ; +}; diff --git a/src/renderer/globalShortcuts/recordKeybind.ts b/src/renderer/globalShortcuts/recordKeybind.ts index d356980..eef00de 100644 --- a/src/renderer/globalShortcuts/recordKeybind.ts +++ b/src/renderer/globalShortcuts/recordKeybind.ts @@ -90,7 +90,7 @@ export async function recordKeybind(signal?: AbortSignal): Promise { } function cleanup() { - document.removeEventListener("keydown", onKeyDown); + document.removeEventListener("keydown", onKeyDown, { capture: true }); signal?.removeEventListener("abort", onAbort); } @@ -101,6 +101,7 @@ export async function recordKeybind(signal?: AbortSignal): Promise { function onKeyDown(event: KeyboardEvent) { event.preventDefault(); + event.stopImmediatePropagation(); if (MODIFIER_KEYS.has(event.key)) return; @@ -120,6 +121,6 @@ export async function recordKeybind(signal?: AbortSignal): Promise { } signal?.addEventListener("abort", onAbort, { once: true }); - document.addEventListener("keydown", onKeyDown); + document.addEventListener("keydown", onKeyDown, { capture: true }); }); } diff --git a/src/renderer/ipcCommands.ts b/src/renderer/ipcCommands.ts index 9625e1a..cbb1842 100644 --- a/src/renderer/ipcCommands.ts +++ b/src/renderer/ipcCommands.ts @@ -54,4 +54,4 @@ onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages); onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker)); -onIpcCommand(IpcCommands.HANDLE_KEY_BIND, handleKeyBind); +onIpcCommand(IpcCommands.KEY_BINDS_HANDLE, handleKeyBind); diff --git a/src/renderer/patches/keybindButton.tsx b/src/renderer/patches/keybindButton.tsx new file mode 100644 index 0000000..4541322 --- /dev/null +++ b/src/renderer/patches/keybindButton.tsx @@ -0,0 +1,27 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2026 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Button, ErrorBoundary } from "@vencord/types/components"; +import { openKeybindsModal } from "renderer/globalShortcuts/ShortcutSettings"; + +import { addPatch } from "./shared"; + +addPatch({ + patches: [ + { + find: "#{intl::KEYBIND_IN_BROSWER_NOTICE}", + replacement: { + match: /:(.{0,10}jsx.{0,50}messageType:.{0,100}#{intl::KEYBIND_IN_BROSWER_NOTICE})/, + replace: ":true?$self.renderKeybindsButton():$1" + } + } + ], + + renderKeybindsButton: ErrorBoundary.wrap( + () => , + { noop: true } + ) +}); diff --git a/src/renderer/reactiveState.ts b/src/renderer/reactiveState.ts new file mode 100644 index 0000000..898d7b8 --- /dev/null +++ b/src/renderer/reactiveState.ts @@ -0,0 +1,40 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2026 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { useForceUpdater } from "@vencord/types/utils"; +import { useEffect } from "@vencord/types/webpack/common"; + +interface ReactiveValue { + value: T; + use(): T; +} + +export function reactiveValue(initialValue: T): ReactiveValue; +export function reactiveValue(initialValue?: T): ReactiveValue; +export function reactiveValue(initialValue?: T) { + const changeListeners = new Set<() => void>(); + let value = initialValue; + + return { + get value() { + return value; + }, + set value(newValue) { + value = newValue; + changeListeners.forEach(cb => cb()); + }, + use() { + const update = useForceUpdater(); + + useEffect(() => { + changeListeners.add(update); + return () => void changeListeners.delete(update); + }, []); + + return value; + } + }; +} diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 75e9941..6ab52e7 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -85,5 +85,6 @@ export const enum IpcCommands { SCREEN_SHARE_PICKER = "screenshare:picker", - HANDLE_KEY_BIND = "keyBinds:handle" + KEY_BINDS_HANDLE = "keyBinds:handle", + KEY_BINDS_SET_STATUS = "keyBinds:setStatus" }