fixes etc

This commit is contained in:
Vendicated
2026-04-05 04:07:23 +02:00
parent f3e01c2ba4
commit cf3667c151
12 changed files with 150 additions and 22 deletions
+2
View File
@@ -13,3 +13,5 @@ export const AppEvents = new EventEmitter<{
userAssetChanged: [UserAssetType]; userAssetChanged: [UserAssetType];
setTrayVariant: ["tray" | "trayUnread"]; setTrayVariant: ["tray" | "trayUnread"];
}>(); }>();
export const appReady = new Promise<void>(resolve => AppEvents.once("appLoaded", () => resolve()));
+1 -1
View File
@@ -167,5 +167,5 @@ app.on("second-instance", (_e, _argv, _cwd, data) => {
const { args } = data as { args: typeof CommandLine; IS_DEV: boolean }; const { args } = data as { args: typeof CommandLine; IS_DEV: boolean };
const { "run-shortcut": shortcut } = args.values; const { "run-shortcut": shortcut } = args.values;
if (shortcut) sendRendererCommand(IpcCommands.HANDLE_KEY_BIND, shortcut); if (shortcut) sendRendererCommand(IpcCommands.KEY_BINDS_HANDLE, shortcut);
}); });
+4 -1
View File
@@ -8,6 +8,7 @@ import { randomUUID } from "crypto";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
import { appReady } from "./events";
import { mainWin } from "./mainWindow"; import { mainWin } from "./mainWindow";
const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>(); const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>();
@@ -30,7 +31,9 @@ export interface IpcResponse {
* *
* You must add a handler for the message in the renderer process. * You must add a handler for the message in the renderer process.
*/ */
export function sendRendererCommand<T = any>(message: string, data?: any) { export async function sendRendererCommand<T = any>(message: string, data?: any) {
await appReady;
if (mainWin.isDestroyed()) { if (mainWin.isDestroyed()) {
console.warn("Main window is destroyed, cannot send IPC command:", message); console.warn("Main window is destroyed, cannot send IPC command:", message);
return Promise.reject(new Error("Main window is destroyed")); return Promise.reject(new Error("Main window is destroyed"));
+7 -5
View File
@@ -12,7 +12,6 @@ import { Settings } from "./settings";
function unregisterKeyBinds() { function unregisterKeyBinds() {
if (!app.isReady()) return; if (!app.isReady()) return;
globalShortcut.unregisterAll(); globalShortcut.unregisterAll();
} }
@@ -21,15 +20,18 @@ export async function registerKeyBinds() {
unregisterKeyBinds(); unregisterKeyBinds();
let success = true;
for (const { action, enabled, key } of Settings.store.keyBinds || []) { for (const { action, enabled, key } of Settings.store.keyBinds || []) {
if (!enabled || !key || action === "unassigned") continue; if (!enabled || !key || action === "unassigned") continue;
console.log( const registeredSuccessfully = globalShortcut.register(key, () =>
key, sendRendererCommand(IpcCommands.KEY_BINDS_HANDLE, action)
"SUCCESS",
globalShortcut.register(key, () => sendRendererCommand(IpcCommands.HANDLE_KEY_BIND, action))
); );
success &&= registeredSuccessfully;
} }
sendRendererCommand(IpcCommands.KEY_BINDS_SET_STATUS, success);
} }
Settings.addChangeListener("keyBinds", registerKeyBinds); Settings.addChangeListener("keyBinds", registerKeyBinds);
@@ -9,7 +9,7 @@ import "./settings.css";
import { classNameFactory } from "@vencord/types/api/Styles"; import { classNameFactory } from "@vencord/types/api/Styles";
import { BaseText, Divider, ErrorBoundary } from "@vencord/types/components"; import { BaseText, Divider, ErrorBoundary } from "@vencord/types/components";
import { ComponentType } from "react"; import { ComponentType } from "react";
import { KeyBindSettings } from "renderer/globalShortcuts/ShortcutSettings"; import { KeybindsButton } from "renderer/globalShortcuts/ShortcutSettings";
import { Settings, useSettings } from "renderer/settings"; import { Settings, useSettings } from "renderer/settings";
import { isMac, isWindows } from "renderer/utils"; import { isMac, isWindows } from "renderer/utils";
@@ -123,7 +123,8 @@ const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>>
title: "Disable smooth scrolling", title: "Disable smooth scrolling",
description: "Disables smooth scrolling", description: "Disables smooth scrolling",
defaultValue: false defaultValue: false
} },
KeybindsButton
], ],
Notifications: [ Notifications: [
NotificationBadgeToggle, NotificationBadgeToggle,
@@ -149,7 +150,6 @@ const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>>
defaultValue: false defaultValue: false
} }
], ],
KeyBinds: [KeyBindSettings],
"Developer Options": [DeveloperOptionsButton] "Developer Options": [DeveloperOptionsButton]
}; };
@@ -1,6 +1,13 @@
.vcd-shortcuts-container {
display: flex;
flex-direction: column;
gap: 1em;
padding-block: 1em;
}
.vcd-shortcuts-group { .vcd-shortcuts-group {
display: grid; display: grid;
gap: 0.25em 1em; gap: 0.25em 0.5em;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto auto; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto auto;
align-items: center; align-items: center;
} }
@@ -21,7 +28,8 @@
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
width: 100%; width: 100%;
min-height: calc(var(--select-option-height) - 2px); min-height: calc(var(--select-option-height) - 2px);
padding: 0.25em; padding: 3px 4px 3px 12px;
box-sizing: border-box;
} }
.vcd-shortcuts-record-input { .vcd-shortcuts-record-input {
@@ -6,10 +6,23 @@
import "./ShortcutSettings.css"; import "./ShortcutSettings.css";
import { BaseText, Button, DeleteIcon, Switch } from "@vencord/types/components"; import { BaseText, Button, Card, DeleteIcon, Switch } from "@vencord/types/components";
import { classNameFactory, identity } from "@vencord/types/utils"; import {
classNameFactory,
identity,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalRoot,
ModalSize,
openModal
} from "@vencord/types/utils";
import { React, Select, useState } from "@vencord/types/webpack/common"; 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 { Settings } from "renderer/settings";
import { IpcCommands } from "shared/IpcEvents";
import { recordKeybind } from "./recordKeybind"; import { recordKeybind } from "./recordKeybind";
@@ -20,13 +33,14 @@ const ShortcutActions = {
toggleMute: "Toggle Mute", toggleMute: "Toggle Mute",
toggleDeafen: "Toggle Deafen", toggleDeafen: "Toggle Deafen",
toggleStreamerMode: "Toggle Streamer Mode", toggleStreamerMode: "Toggle Streamer Mode",
toggleCamera: "Toggle Camera", disconnectFromVoiceChannel: "Disconnect from VC"
toggleScreenShare: "Toggle Screen Share",
disconnectFromVoiceChannel: "Disconnect from Voice Channel"
} as const; } as const;
export type ShortcutAction = keyof typeof ShortcutActions; export type ShortcutAction = keyof typeof ShortcutActions;
const shortcutStatus = reactiveValue(true);
onIpcCommand(IpcCommands.KEY_BINDS_SET_STATUS, status => (shortcutStatus.value = status));
export interface KeyBind { export interface KeyBind {
id: string; id: string;
enabled: boolean; enabled: boolean;
@@ -109,9 +123,18 @@ function KeyBindGroup({
export function KeyBindSettings() { export function KeyBindSettings() {
const [keyBinds, setKeyBinds] = useState<KeyBind[]>(Settings.store.keyBinds ?? []); const [keyBinds, setKeyBinds] = useState<KeyBind[]>(Settings.store.keyBinds ?? []);
const shortcutsStatus = shortcutStatus.use();
return ( return (
<div> <div className={cl("container")}>
{shortcutsStatus === false && (
<Card variant="danger">
<BaseText size="md" weight="medium">
Some of your keybinds failed to register.
</BaseText>
</Card>
)}
{keyBinds.map((keyBind, index) => ( {keyBinds.map((keyBind, index) => (
<KeyBindGroup <KeyBindGroup
key={keyBind.id} key={keyBind.id}
@@ -147,3 +170,24 @@ export function KeyBindSettings() {
</div> </div>
); );
} }
export function openKeybindsModal() {
openModal(props => (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalHeader>
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}>
Keybinds
</BaseText>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<KeyBindSettings />
</ModalContent>
</ModalRoot>
));
}
export const KeybindsButton: SettingsComponent = () => {
return <Button onClick={() => openKeybindsModal()}>Configure Keybinds</Button>;
};
@@ -90,7 +90,7 @@ export async function recordKeybind(signal?: AbortSignal): Promise<string> {
} }
function cleanup() { function cleanup() {
document.removeEventListener("keydown", onKeyDown); document.removeEventListener("keydown", onKeyDown, { capture: true });
signal?.removeEventListener("abort", onAbort); signal?.removeEventListener("abort", onAbort);
} }
@@ -101,6 +101,7 @@ export async function recordKeybind(signal?: AbortSignal): Promise<string> {
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation();
if (MODIFIER_KEYS.has(event.key)) return; if (MODIFIER_KEYS.has(event.key)) return;
@@ -120,6 +121,6 @@ export async function recordKeybind(signal?: AbortSignal): Promise<string> {
} }
signal?.addEventListener("abort", onAbort, { once: true }); signal?.addEventListener("abort", onAbort, { once: true });
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown, { capture: true });
}); });
} }
+1 -1
View File
@@ -54,4 +54,4 @@ onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages);
onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker)); onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker));
onIpcCommand(IpcCommands.HANDLE_KEY_BIND, handleKeyBind); onIpcCommand(IpcCommands.KEY_BINDS_HANDLE, handleKeyBind);
+27
View File
@@ -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(
() => <Button onClick={() => openKeybindsModal()}>Configure Vesktop Keybinds</Button>,
{ noop: true }
)
});
+40
View File
@@ -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<T> {
value: T;
use(): T;
}
export function reactiveValue<T>(initialValue: T): ReactiveValue<T>;
export function reactiveValue<T>(initialValue?: T): ReactiveValue<T | undefined>;
export function reactiveValue<T>(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;
}
};
}
+2 -1
View File
@@ -85,5 +85,6 @@ export const enum IpcCommands {
SCREEN_SHARE_PICKER = "screenshare:picker", SCREEN_SHARE_PICKER = "screenshare:picker",
HANDLE_KEY_BIND = "keyBinds:handle" KEY_BINDS_HANDLE = "keyBinds:handle",
KEY_BINDS_SET_STATUS = "keyBinds:setStatus"
} }