From 65fbb3da2bd90221f0abcc09b4deddeb9aab2e9d Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 1 Feb 2024 20:16:59 +0000 Subject: [PATCH 01/10] feat(plugin): commandPalette --- src/plugins/commandPalette/README.md | 52 ++++++++ src/plugins/commandPalette/commands.ts | 26 ++++ .../components/CommandPalette.tsx | 107 +++++++++++++++ .../components/MultipleChoice.tsx | 122 ++++++++++++++++++ .../commandPalette/components/TextInput.tsx | 56 ++++++++ .../commandPalette/components/styles.css | 30 +++++ src/plugins/commandPalette/index.ts | 43 ++++++ src/utils/constants.ts | 4 + 8 files changed, 440 insertions(+) create mode 100644 src/plugins/commandPalette/README.md create mode 100644 src/plugins/commandPalette/commands.ts create mode 100644 src/plugins/commandPalette/components/CommandPalette.tsx create mode 100644 src/plugins/commandPalette/components/MultipleChoice.tsx create mode 100644 src/plugins/commandPalette/components/TextInput.tsx create mode 100644 src/plugins/commandPalette/components/styles.css create mode 100644 src/plugins/commandPalette/index.ts diff --git a/src/plugins/commandPalette/README.md b/src/plugins/commandPalette/README.md new file mode 100644 index 0000000000..0f3065f1e5 --- /dev/null +++ b/src/plugins/commandPalette/README.md @@ -0,0 +1,52 @@ +# Custom Commands Documentation + +## Adding Custom Commands to Command Palette + +To enhance your application with custom commands in the command palette, you have two options: hardcoding the commands or using the exported `registerAction` function. Both methods share almost identical implementations, as the function requires the same arguments as hardcoding. + +For the examples below, we'll focus on using the `registerAction` function. + +### Implementing Multiple Choice Modals + +```ts +registerAction({ + id: 'multipleChoiceCommand', + label: 'Multiple Choice', + callback: async () => { + const choice = await openMultipleChoice([ + { id: 'test1', label: 'Test 1' }, + { id: 'test2', label: 'Test 2' }, + ]); + + console.log(`Selected ${choice.label} with the ID ${choice.id}`); + }, +}); +``` + +**ID**: A unique identifier for the command. Ensure it is unique across all commands. + +**Label**: The text that will be displayed in the command palette. + +**Callback**: The function that executes when the command is triggered. + +Inside the callback, we use the `openMultipleChoice` function, which opens a modal with a list of choices. Users can then select an option, and the function returns a `ButtonAction`, which is the same object used in the `registerAction` function. + +Finally, we log the user's choice for reference. + +### Implementing String Input Modals +To allow users to input a string, you can use the following example: + +```ts +registerAction({ + id: 'stringInputCommand', + label: 'String Input', + callback: async () => { + const text = await openSimpleTextInput(); + console.log(`They typed: ${text}`); + }, +}); +``` + +In this example, when the 'String Input' command is triggered, a modal with a simple text input field appears. The user can input text, and the entered string is then logged for further processing. + +Remember to replace 'stringInputCommand' and 'String Input' with your desired unique identifier and display label. This allows you to customize the command according to your application's needs. diff --git a/src/plugins/commandPalette/commands.ts b/src/plugins/commandPalette/commands.ts new file mode 100644 index 0000000000..8f843bbf4e --- /dev/null +++ b/src/plugins/commandPalette/commands.ts @@ -0,0 +1,26 @@ +import { relaunch, showItemInFolder } from "@utils/native"; +import { SettingsRouter } from "@webpack/common"; + +export interface ButtonAction { + id: string; + label: string; + callback?: () => void; +} + +export let actions: ButtonAction[] = [ + { id: 'openVencordSettings', label: 'Open Vencord tab', callback: async () => await SettingsRouter.open("VencordSettings") }, + { id: 'openPluginSettings', label: 'Open Plugin tab', callback: () => SettingsRouter.open("VencordPlugins") }, + { id: 'openThemesSettings', label: 'Open Themes tab', callback: () => SettingsRouter.open("VencordThemes") }, + { id: 'openUpdaterSettings', label: 'Open Updater tab', callback: () => SettingsRouter.open("VencordUpdater") }, + { id: 'openVencordCloudSettings', label: 'Open Cloud tab', callback: () => SettingsRouter.open("VencordCloud") }, + { id: 'openBackupSettings', label: 'Open Backup & Restore tab', callback: () => SettingsRouter.open("VencordSettingsSync") }, + { id: 'restartClient', label: 'Restart Client', callback: () => relaunch() }, + { id: 'openQuickCSSFile', label: 'Open Quick CSS File', callback: () => VencordNative.quickCss.openEditor() }, + { id: 'openSettingsFolder', label: 'Open Settings Folder', callback: async () => showItemInFolder(await VencordNative.settings.getSettingsDir()) }, + { id: 'openInGithub', label: 'Open in Github', callback: () => VencordNative.native.openExternal("https://github.com/Vendicated/Vencord") } +]; + +export function registerAction(action: ButtonAction) { + actions.push(action); +} + diff --git a/src/plugins/commandPalette/components/CommandPalette.tsx b/src/plugins/commandPalette/components/CommandPalette.tsx new file mode 100644 index 0000000000..ba96b4194c --- /dev/null +++ b/src/plugins/commandPalette/components/CommandPalette.tsx @@ -0,0 +1,107 @@ +import { classNameFactory } from "@api/Styles"; +import { Logger } from "@utils/Logger"; +import { closeAllModals, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { React, TextInput, useState } from "@webpack/common"; +import { useEffect } from "@webpack/common"; +import { actions } from "../commands"; + +import "./styles.css"; + +const logger = new Logger("CommandPalette", "#e5c890"); + +export function CommandPalette({ modalProps }) { + const cl = classNameFactory("vc-command-palette-"); + const [queryEh, setQuery] = useState(""); + const [focusedIndex, setFocusedIndex] = useState(null); + const [startIndex, setStartIndex] = useState(0); + + const sortedActions = actions.slice().sort((a, b) => a.label.localeCompare(b.label)); + + const filteredActions = sortedActions.filter( + (action) => action.label.toLowerCase().includes(queryEh.toLowerCase()) + ); + + const visibleActions = filteredActions.slice(startIndex, startIndex + 20); + + const totalActions = filteredActions.length; + + const handleButtonClick = (actionId: string, index: number) => { + const selectedAction = filteredActions.find((action) => action.id === actionId); + + if (selectedAction) { + logger.log(`${selectedAction.id}'s action was triggered.`); + } + + closeAllModals(); + + selectedAction?.callback?.(); + setFocusedIndex(index); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + const currentIndex = focusedIndex !== null ? focusedIndex : -1; + let nextIndex; + + switch (e.key) { + case 'ArrowUp': + e.preventDefault(); + nextIndex = currentIndex > 0 ? currentIndex - 1 : visibleActions.length - 1; + setFocusedIndex(nextIndex); + + if (currentIndex === 0 && totalActions > 20) { + setStartIndex((prev) => Math.max(prev - 1, 0)); + setFocusedIndex(0); + } + + break; + case 'ArrowDown': + e.preventDefault(); + nextIndex = currentIndex < visibleActions.length - 1 ? currentIndex + 1 : 0; + setFocusedIndex(nextIndex); + + if (currentIndex === visibleActions.length - 1 && totalActions > 20) { + setStartIndex((prev) => Math.min(prev + 1, filteredActions.length - 20)); + setFocusedIndex(19); + } + break; + case 'Enter': + if (currentIndex !== null && currentIndex >= 0 && currentIndex < visibleActions.length) { + handleButtonClick(visibleActions[currentIndex].id, currentIndex); + } + break; + default: + break; + } + }; + + useEffect(() => { + setFocusedIndex(0); + setStartIndex(0); + }, [queryEh]); + + return ( + +
+ setQuery(e)} + style={{ width: "100%", borderRadius: "0" }} + placeholder="Search the Command Palette" + /> +
+ {visibleActions.map((action, index) => ( + + ))} +
+
+
+ ); +} + +export const openCommandPalette = () => openModal((modalProps) => ); diff --git a/src/plugins/commandPalette/components/MultipleChoice.tsx b/src/plugins/commandPalette/components/MultipleChoice.tsx new file mode 100644 index 0000000000..5d722fc9f0 --- /dev/null +++ b/src/plugins/commandPalette/components/MultipleChoice.tsx @@ -0,0 +1,122 @@ +import { classNameFactory } from "@api/Styles"; +import { Logger } from "@utils/Logger"; +import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { React, TextInput, useState } from "@webpack/common"; +import { useEffect } from "@webpack/common"; +import { ButtonAction } from "../commands"; + +import "./styles.css"; + +interface MultipleChoiceProps { + modalProps: ModalProps; + onSelect: (selectedValue: any) => void; + choices: ButtonAction[]; +} + +export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoiceProps) { + const cl = classNameFactory("vc-command-palette-"); + const [queryEh, setQuery] = useState(""); + const [focusedIndex, setFocusedIndex] = useState(null); + const [startIndex, setStartIndex] = useState(0); + + const sortedActions = choices.slice().sort((a, b) => a.label.localeCompare(b.label)); + + const filteredActions = sortedActions.filter( + (action) => action.label.toLowerCase().includes(queryEh.toLowerCase()) + ); + + + const visibleActions = filteredActions.slice(startIndex, startIndex + 20); + + const totalActions = filteredActions.length; + + const handleButtonClick = (actionId: string, index: number) => { + const selectedAction = filteredActions.find((action) => action.id === actionId); + + if (selectedAction) { + onSelect(selectedAction); + } + + closeAllModals(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + const currentIndex = focusedIndex !== null ? focusedIndex : -1; + let nextIndex; + + switch (e.key) { + case 'ArrowUp': + e.preventDefault(); + nextIndex = currentIndex > 0 ? currentIndex - 1 : visibleActions.length - 1; + setFocusedIndex(nextIndex); + + if (currentIndex === 0 && totalActions > 20) { + setStartIndex((prev) => Math.max(prev - 1, 0)); + setFocusedIndex(0); + } + + break; + case 'ArrowDown': + e.preventDefault(); + nextIndex = currentIndex < visibleActions.length - 1 ? currentIndex + 1 : 0; + setFocusedIndex(nextIndex); + + if (currentIndex === visibleActions.length - 1 && totalActions > 20) { + setStartIndex((prev) => Math.min(prev + 1, filteredActions.length - 20)); + setFocusedIndex(19); + } + break; + case 'Enter': + if (currentIndex !== null && currentIndex >= 0 && currentIndex < visibleActions.length) { + handleButtonClick(visibleActions[currentIndex].id, currentIndex); + } + break; + default: + break; + } + }; + + useEffect(() => { + setFocusedIndex(0); + setStartIndex(0); + }, [queryEh]); + + return ( + +
+ setQuery(e)} + style={{ width: "100%", borderRadius: "0" }} + placeholder="Search the Command Palette" + /> +
+ {visibleActions.map((action, index) => ( + + ))} +
+
+
+ ); +} + +export function openMultipleChoice(choices: ButtonAction[]): Promise { + return new Promise((resolve) => { + openModal((modalProps) => ( + { + closeAllModals(); + resolve(selectedValue); + }} + choices={choices} + /> + )); + }); +} diff --git a/src/plugins/commandPalette/components/TextInput.tsx b/src/plugins/commandPalette/components/TextInput.tsx new file mode 100644 index 0000000000..783d84b791 --- /dev/null +++ b/src/plugins/commandPalette/components/TextInput.tsx @@ -0,0 +1,56 @@ +import { classNameFactory } from "@api/Styles"; +import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { React, TextInput, useState } from "@webpack/common"; +import { useEffect } from "@webpack/common"; + +import "./styles.css"; + +interface SimpleTextInputProps { + modalProps: ModalProps; + onSelect: (inputValue: string) => void; + placeholder?: string; +} + +export function SimpleTextInput({ modalProps, onSelect, placeholder }: SimpleTextInputProps) { + const cl = classNameFactory("vc-command-palette-"); + const [inputValue, setInputValue] = useState(""); + + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'Enter': + onSelect(inputValue); + closeAllModals(); + break; + default: + break; + } + }; + + useEffect(() => { + setInputValue(""); + }, []); + + return ( + + setInputValue(e as unknown as string)} + style={{ width: "405px", borderRadius: "15px" }} + placeholder={placeholder ?? "Type and press Enter"} + /> + + ); +} + + +export function openSimpleTextInput(placeholder?: string): Promise { + return new Promise((resolve) => { + openModal((modalProps) => ( + resolve(inputValue)} + placeholder={placeholder} + /> + )); + }); +} diff --git a/src/plugins/commandPalette/components/styles.css b/src/plugins/commandPalette/components/styles.css new file mode 100644 index 0000000000..a920412535 --- /dev/null +++ b/src/plugins/commandPalette/components/styles.css @@ -0,0 +1,30 @@ +/* cl = vc-command-palette- */ + +.vc-command-palette-root { + border-radius: 10px; +} + +.vc-command-palette-option { + padding: 5px; + background-color: var(--background-tertiary); + color: var(--white-500); + font-family: var(--font-display); +} + +/* .vc-command-palette-option:hover { + color: var(--interactive-hover); + border: 1.5px solid var(--interactive-hover); +} */ + +.vc-command-palette-key-hover { + padding: 5px; + background-color: var(--background-tertiary); + font-family: var(--font-display); + color: var(--interactive-hover); + border: 1.5px solid var(--interactive-hover); +} + +.vc-command-palette-option-container{ + display: grid; + gap: 2px; +} diff --git a/src/plugins/commandPalette/index.ts b/src/plugins/commandPalette/index.ts new file mode 100644 index 0000000000..81e508e615 --- /dev/null +++ b/src/plugins/commandPalette/index.ts @@ -0,0 +1,43 @@ +import definePlugin from "@utils/types"; +import { openCommandPalette } from "./components/CommandPalette"; +import { closeAllModals } from "@utils/modal"; +import { SettingsRouter } from "@webpack/common"; +import { registerAction } from "./commands"; +import { Devs } from "@utils/constants"; + + +export default definePlugin({ + name: "CommandPalette", + description: "Allows you to navigate the UI with a keyboard.", + authors: [Devs.Ethan], + + start() { + document.addEventListener("keydown", this.event); + + if (IS_DEV) { + registerAction({ + id: 'openDevSettings', + label: 'Open Dev tab', + callback: () => SettingsRouter.open("VencordPatchHelper") + }); + } + }, + + stop() { + document.removeEventListener("keydown", this.event); + }, + + event(e: KeyboardEvent) { + const { ctrlKey, shiftKey, key } = e; + + if (!ctrlKey || !shiftKey || key !== "P") return; + + closeAllModals(); + + if (document.querySelector(".vc-command-palette-root")) { // Allows for a toggle + return; + } + + openCommandPalette(); + } +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8999361284..a84475a7a9 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -410,6 +410,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ coolelectronics: { name: "coolelectronics", id: 696392247205298207n, + }, + Ethan: { + name: "Ethan", + id: 721717126523781240n } } satisfies Record); From 4f4e368c283dfa96c921bdd0e156edabef7c641f Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 3 Feb 2024 17:54:18 +0000 Subject: [PATCH 02/10] feat: added more default commands --- src/plugins/commandPalette/commands.ts | 152 +++++++++++++++++- .../commandPalette/components/TextInput.tsx | 4 +- 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/src/plugins/commandPalette/commands.ts b/src/plugins/commandPalette/commands.ts index 8f843bbf4e..c6cffa8c96 100644 --- a/src/plugins/commandPalette/commands.ts +++ b/src/plugins/commandPalette/commands.ts @@ -1,5 +1,25 @@ import { relaunch, showItemInFolder } from "@utils/native"; -import { SettingsRouter } from "@webpack/common"; +import { GuildStore, NavigationRouter, SettingsRouter, Toasts, UserStore } from "@webpack/common"; +import { openSimpleTextInput } from "./components/TextInput"; +import { checkForUpdates, getRepo } from "@utils/updater"; +import Plugins from "~plugins"; +import { Settings } from "@api/Settings"; +import { openMultipleChoice } from "./components/MultipleChoice"; +import { Clipboard } from "@webpack/common"; +import { showNotification } from "@api/Notifications"; +import { PresenceStore, Tooltip } from "@webpack/common"; +import { findStore } from "@webpack"; +import { FluxDispatcher } from "@webpack/common"; +import { ChannelStore } from "@webpack/common"; + +const selfPresenceStore = findStore("SelfPresenceStore"); + +enum Status { + ONLINE = "online", + IDLE = "idle", + DND = "dnd", + INVISIBLE = "invisible" +} export interface ButtonAction { id: string; @@ -17,7 +37,135 @@ export let actions: ButtonAction[] = [ { id: 'restartClient', label: 'Restart Client', callback: () => relaunch() }, { id: 'openQuickCSSFile', label: 'Open Quick CSS File', callback: () => VencordNative.quickCss.openEditor() }, { id: 'openSettingsFolder', label: 'Open Settings Folder', callback: async () => showItemInFolder(await VencordNative.settings.getSettingsDir()) }, - { id: 'openInGithub', label: 'Open in Github', callback: () => VencordNative.native.openExternal("https://github.com/Vendicated/Vencord") } + { id: 'openInGithub', label: 'Open in Github', callback: async () => VencordNative.native.openExternal(await getRepo()) }, + + { + id: 'openInBrowser', label: 'Open in Browser', callback: async () => { + const url = await openSimpleTextInput("Enter a URL"); + const newUrl = url.replace(/(https?:\/\/)?([a-zA-Z0-9-]+)\.([a-zA-Z0-9-]+)/, "https://$2.$3"); + + try { + new URL(newUrl); // Throws if invalid + VencordNative.native.openExternal(newUrl); + } catch { + Toasts.show({ + message: "Invalid URL", + type: Toasts.Type.FAILURE, + id: Toasts.genId(), + options: { + position: Toasts.Position.BOTTOM + } + }); + } + } + }, + + { + id: 'togglePlugin', label: 'Toggle Plugin', callback: async () => { + const plugins = Object.keys(Plugins); + let options: ButtonAction[] = []; + + for (const plugin of plugins) { + options.push({ + id: plugin, + label: plugin + }); + } + + const choice = await openMultipleChoice(options); + + const enabled = await openMultipleChoice([ + { id: 'enable', label: 'Enable' }, + { id: 'disable', label: 'Disable' } + ]); + + if (choice) { + if (enabled.id === 'enable') { + Settings.plugins[choice.id].enabled = true; + } else { + Settings.plugins[choice.id].enabled = false; + } + } + } + }, + + { + id: 'quickFetch', label: 'Quick Fetch', callback: async () => { + try { + const url = await openSimpleTextInput("Enter URL to fetch (GET only)"); + const newUrl = url.replace(/(https?:\/\/)?([a-zA-Z0-9-]+)\.([a-zA-Z0-9-]+)/, "https://$2.$3"); + const res = (await fetch(newUrl)); + const text = await res.text(); + Clipboard.copy(text); + + Toasts.show({ + message: "Copied response to clipboard!", + type: Toasts.Type.SUCCESS, + id: Toasts.genId(), + options: { + position: Toasts.Position.BOTTOM + } + }); + + } catch (e) { + Toasts.show({ + message: "Issue fetching URL", + type: Toasts.Type.FAILURE, + id: Toasts.genId(), + options: { + position: Toasts.Position.BOTTOM + } + }); + } + } + }, + + { + id: 'checkForUpdates', label: 'Check for Updates', callback: async () => { + const isOutdated = await checkForUpdates(); + + if (isOutdated) { + setTimeout(() => showNotification({ + title: "A Vencord update is available!", + body: "Click here to view the update", + permanent: true, + noPersist: true, + onClick() { + SettingsRouter.open("VencordUpdater"); + } + }), 10_000); + } else { + Toasts.show({ + message: "No updates available", + type: Toasts.Type.MESSAGE, + id: Toasts.genId(), + options: { + position: Toasts.Position.BOTTOM + } + }); + } + } + }, + + { + id: 'navToServer', label: 'Navigate to Server', callback: async () => { + const allServers = Object.values(GuildStore.getGuilds()); + let options: ButtonAction[] = []; + + for (const server of allServers) { + options.push({ + id: server.id, + label: server.name + }); + } + + const choice = await openMultipleChoice(options); + + if (choice) { + NavigationRouter.transitionToGuild(choice.id); + } + } + } ]; export function registerAction(action: ButtonAction) { diff --git a/src/plugins/commandPalette/components/TextInput.tsx b/src/plugins/commandPalette/components/TextInput.tsx index 783d84b791..d7bfbdbe66 100644 --- a/src/plugins/commandPalette/components/TextInput.tsx +++ b/src/plugins/commandPalette/components/TextInput.tsx @@ -31,11 +31,11 @@ export function SimpleTextInput({ modalProps, onSelect, placeholder }: SimpleTex }, []); return ( - + setInputValue(e as unknown as string)} - style={{ width: "405px", borderRadius: "15px" }} + style={{ width: "405px", borderRadius: "1px" }} placeholder={placeholder ?? "Type and press Enter"} /> From eecc7fa8acec7d971eedc9d73cfdd86f0b5dc723 Mon Sep 17 00:00:00 2001 From: Ethan Davies Date: Sat, 3 Feb 2024 18:20:48 +0000 Subject: [PATCH 03/10] style: improved palette option alignment --- src/plugins/commandPalette/components/styles.css | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/plugins/commandPalette/components/styles.css b/src/plugins/commandPalette/components/styles.css index a920412535..2fa42f4943 100644 --- a/src/plugins/commandPalette/components/styles.css +++ b/src/plugins/commandPalette/components/styles.css @@ -9,13 +9,10 @@ background-color: var(--background-tertiary); color: var(--white-500); font-family: var(--font-display); + text-align: left; + padding-left: 0.8rem; } -/* .vc-command-palette-option:hover { - color: var(--interactive-hover); - border: 1.5px solid var(--interactive-hover); -} */ - .vc-command-palette-key-hover { padding: 5px; background-color: var(--background-tertiary); From e0e18f121177b233e4fb36ab7a7ac60bef7ea965 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 3 Feb 2024 19:10:37 +0000 Subject: [PATCH 04/10] refactor: improved docs and removed an unnessacary modal addition --- docs/3_COMMAND_PALETTE.md | 49 +++++++++++++++++ src/plugins/commandPalette/README.md | 52 ------------------- .../components/MultipleChoice.tsx | 2 +- .../commandPalette/components/TextInput.tsx | 2 +- 4 files changed, 51 insertions(+), 54 deletions(-) create mode 100644 docs/3_COMMAND_PALETTE.md delete mode 100644 src/plugins/commandPalette/README.md diff --git a/docs/3_COMMAND_PALETTE.md b/docs/3_COMMAND_PALETTE.md new file mode 100644 index 0000000000..014b92832c --- /dev/null +++ b/docs/3_COMMAND_PALETTE.md @@ -0,0 +1,49 @@ +# Command Palette Actions Guide + +Welcome to the Command Palette Actions Guide! This guide serves to inform you on how to implement your own actions to the CommandPalette plugin. To do so you have two options: hardcoding or utilizing the `registerAction` function. This guide will focus on using the function as its best practice and is what should be used in most situations. + +> [!IMPORTANT] +> While both methods hardcoding and using `registerAction` offer similar implementations, it's recommended to refrain from Hardcoding unless you solely plan on using this locally. + +### Implementing Multiple Choice Modals + +```ts +registerAction({ + id: 'multipleChoiceCommand', + label: 'Multiple Choice', + callback: async () => { + // Open a modal with multiple choices + const choice = await openMultipleChoice([ + { id: 'test1', label: 'Test 1' }, + { id: 'test2', label: 'Test 2' }, + ]); + + // Log the selected choice with its label and ID + console.log(`Selected ${choice.label} with the ID ${choice.id}`); + }, +}); +``` + +- **ID**: A unique identifier for the command/action, ensuring uniqueness across specific options in that modal instance. +- **Label**: The text displayed in the command palette for user recognition. +- **Callback (optional)**: The function executed when the command is triggered. + +Inside the callback, the `openMultipleChoice` function opens a modal with a list of choices. Users can select an option, and upon choosing, a `ButtonAction` type is returned. The user's choice is then logged for reference. + +### Implementing String Input Modals + +```ts +registerAction({ + id: 'stringInputCommand', + label: 'String Input', + callback: async () => { + // Open a modal with a text input + const text = await openSimpleTextInput(); + + // Log the inputted text to console + console.log(`They typed: ${text}`); + }, +}); +``` + +When the `stringInputCommand` is triggered, a modal with a simple text input field appears. Users can input text, and the entered string is returned. In this case, we log their input to console. diff --git a/src/plugins/commandPalette/README.md b/src/plugins/commandPalette/README.md deleted file mode 100644 index 0f3065f1e5..0000000000 --- a/src/plugins/commandPalette/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Custom Commands Documentation - -## Adding Custom Commands to Command Palette - -To enhance your application with custom commands in the command palette, you have two options: hardcoding the commands or using the exported `registerAction` function. Both methods share almost identical implementations, as the function requires the same arguments as hardcoding. - -For the examples below, we'll focus on using the `registerAction` function. - -### Implementing Multiple Choice Modals - -```ts -registerAction({ - id: 'multipleChoiceCommand', - label: 'Multiple Choice', - callback: async () => { - const choice = await openMultipleChoice([ - { id: 'test1', label: 'Test 1' }, - { id: 'test2', label: 'Test 2' }, - ]); - - console.log(`Selected ${choice.label} with the ID ${choice.id}`); - }, -}); -``` - -**ID**: A unique identifier for the command. Ensure it is unique across all commands. - -**Label**: The text that will be displayed in the command palette. - -**Callback**: The function that executes when the command is triggered. - -Inside the callback, we use the `openMultipleChoice` function, which opens a modal with a list of choices. Users can then select an option, and the function returns a `ButtonAction`, which is the same object used in the `registerAction` function. - -Finally, we log the user's choice for reference. - -### Implementing String Input Modals -To allow users to input a string, you can use the following example: - -```ts -registerAction({ - id: 'stringInputCommand', - label: 'String Input', - callback: async () => { - const text = await openSimpleTextInput(); - console.log(`They typed: ${text}`); - }, -}); -``` - -In this example, when the 'String Input' command is triggered, a modal with a simple text input field appears. The user can input text, and the entered string is then logged for further processing. - -Remember to replace 'stringInputCommand' and 'String Input' with your desired unique identifier and display label. This allows you to customize the command according to your application's needs. diff --git a/src/plugins/commandPalette/components/MultipleChoice.tsx b/src/plugins/commandPalette/components/MultipleChoice.tsx index 5d722fc9f0..052aa01668 100644 --- a/src/plugins/commandPalette/components/MultipleChoice.tsx +++ b/src/plugins/commandPalette/components/MultipleChoice.tsx @@ -82,7 +82,7 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice }, [queryEh]); return ( - +
+ setInputValue(e as unknown as string)} From 039f7aee330d7765f952f1aaea3bea7c634f40ac Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 4 Feb 2024 01:24:19 +0000 Subject: [PATCH 05/10] refactor: general improvements --- .../{commands.ts => commands.tsx} | 34 ++++++++++--------- .../components/CommandPalette.tsx | 2 +- .../components/MultipleChoice.tsx | 4 +-- .../commandPalette/components/TextInput.tsx | 13 +++---- .../commandPalette/components/styles.css | 15 ++++++++ 5 files changed, 43 insertions(+), 25 deletions(-) rename src/plugins/commandPalette/{commands.ts => commands.tsx} (90%) diff --git a/src/plugins/commandPalette/commands.ts b/src/plugins/commandPalette/commands.tsx similarity index 90% rename from src/plugins/commandPalette/commands.ts rename to src/plugins/commandPalette/commands.tsx index c6cffa8c96..43cb2ec278 100644 --- a/src/plugins/commandPalette/commands.ts +++ b/src/plugins/commandPalette/commands.tsx @@ -1,5 +1,5 @@ import { relaunch, showItemInFolder } from "@utils/native"; -import { GuildStore, NavigationRouter, SettingsRouter, Toasts, UserStore } from "@webpack/common"; +import { Alerts, GuildStore, NavigationRouter, Parser, SettingsRouter, Toasts, UserStore, React } from "@webpack/common"; import { openSimpleTextInput } from "./components/TextInput"; import { checkForUpdates, getRepo } from "@utils/updater"; import Plugins from "~plugins"; @@ -11,15 +11,7 @@ import { PresenceStore, Tooltip } from "@webpack/common"; import { findStore } from "@webpack"; import { FluxDispatcher } from "@webpack/common"; import { ChannelStore } from "@webpack/common"; - -const selfPresenceStore = findStore("SelfPresenceStore"); - -enum Status { - ONLINE = "online", - IDLE = "idle", - DND = "dnd", - INVISIBLE = "invisible" -} +import { ChangeList } from "@utils/ChangeList"; export interface ButtonAction { id: string; @@ -79,12 +71,8 @@ export let actions: ButtonAction[] = [ { id: 'disable', label: 'Disable' } ]); - if (choice) { - if (enabled.id === 'enable') { - Settings.plugins[choice.id].enabled = true; - } else { - Settings.plugins[choice.id].enabled = false; - } + if (choice && enabled) { + return togglePlugin(choice, enabled.id === 'enable'); } } }, @@ -168,6 +156,20 @@ export let actions: ButtonAction[] = [ } ]; +function togglePlugin(plugin: ButtonAction, enabled: boolean) { + + Settings.plugins[plugin.id].enabled = enabled; + + Toasts.show({ + message: `Successfully ${enabled ? 'enabled' : 'disabled'} ${plugin.id}`, + type: Toasts.Type.SUCCESS, + id: Toasts.genId(), + options: { + position: Toasts.Position.BOTTOM + } + }); +} + export function registerAction(action: ButtonAction) { actions.push(action); } diff --git a/src/plugins/commandPalette/components/CommandPalette.tsx b/src/plugins/commandPalette/components/CommandPalette.tsx index ba96b4194c..7a47286ef1 100644 --- a/src/plugins/commandPalette/components/CommandPalette.tsx +++ b/src/plugins/commandPalette/components/CommandPalette.tsx @@ -85,7 +85,7 @@ export function CommandPalette({ modalProps }) { setQuery(e)} - style={{ width: "100%", borderRadius: "0" }} + style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0" }} placeholder="Search the Command Palette" />
diff --git a/src/plugins/commandPalette/components/MultipleChoice.tsx b/src/plugins/commandPalette/components/MultipleChoice.tsx index 052aa01668..92e5761a26 100644 --- a/src/plugins/commandPalette/components/MultipleChoice.tsx +++ b/src/plugins/commandPalette/components/MultipleChoice.tsx @@ -82,12 +82,12 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice }, [queryEh]); return ( - +
setQuery(e)} - style={{ width: "100%", borderRadius: "0" }} + style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0" }} placeholder="Search the Command Palette" />
diff --git a/src/plugins/commandPalette/components/TextInput.tsx b/src/plugins/commandPalette/components/TextInput.tsx index c36cbe5be7..265746ed4c 100644 --- a/src/plugins/commandPalette/components/TextInput.tsx +++ b/src/plugins/commandPalette/components/TextInput.tsx @@ -1,4 +1,3 @@ -import { classNameFactory } from "@api/Styles"; import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { React, TextInput, useState } from "@webpack/common"; import { useEffect } from "@webpack/common"; @@ -9,10 +8,10 @@ interface SimpleTextInputProps { modalProps: ModalProps; onSelect: (inputValue: string) => void; placeholder?: string; + info?: string; } -export function SimpleTextInput({ modalProps, onSelect, placeholder }: SimpleTextInputProps) { - const cl = classNameFactory("vc-command-palette-"); +export function SimpleTextInput({ modalProps, onSelect, placeholder, info }: SimpleTextInputProps) { const [inputValue, setInputValue] = useState(""); const handleKeyDown = (e: React.KeyboardEvent) => { @@ -31,25 +30,27 @@ export function SimpleTextInput({ modalProps, onSelect, placeholder }: SimpleTex }, []); return ( - + setInputValue(e as unknown as string)} - style={{ width: "405px", borderRadius: "1px" }} + style={{ width: "30vw", borderRadius: "5px" }} placeholder={placeholder ?? "Type and press Enter"} /> + {info &&
{info}
}
); } -export function openSimpleTextInput(placeholder?: string): Promise { +export function openSimpleTextInput(placeholder?: string, info?: string): Promise { return new Promise((resolve) => { openModal((modalProps) => ( resolve(inputValue)} placeholder={placeholder} + info={info} /> )); }); diff --git a/src/plugins/commandPalette/components/styles.css b/src/plugins/commandPalette/components/styles.css index 2fa42f4943..4893e38f9d 100644 --- a/src/plugins/commandPalette/components/styles.css +++ b/src/plugins/commandPalette/components/styles.css @@ -2,6 +2,7 @@ .vc-command-palette-root { border-radius: 10px; + overflow: hidden; } .vc-command-palette-option { @@ -19,9 +20,23 @@ font-family: var(--font-display); color: var(--interactive-hover); border: 1.5px solid var(--interactive-hover); + padding-left: 0.8rem; } .vc-command-palette-option-container{ display: grid; gap: 2px; } + +.vc-command-palette-textinfo { + font-family: var(--font-display); + color: var(--white-500); + margin-left: 0.8rem; + margin-right: 0.8rem; + padding: 0.8rem 0; +} + +.vc-command-palette-simple-text { + background-color: var(--input-background); + width: 30wh; +} From ec6743209f60f2cc3f406a2f4923251069ed412f Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 4 Feb 2024 01:44:27 +0000 Subject: [PATCH 06/10] styles: improved styles --- src/plugins/commandPalette/components/CommandPalette.tsx | 3 ++- src/plugins/commandPalette/components/MultipleChoice.tsx | 3 ++- src/plugins/commandPalette/components/styles.css | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/plugins/commandPalette/components/CommandPalette.tsx b/src/plugins/commandPalette/components/CommandPalette.tsx index 7a47286ef1..44f3792726 100644 --- a/src/plugins/commandPalette/components/CommandPalette.tsx +++ b/src/plugins/commandPalette/components/CommandPalette.tsx @@ -85,7 +85,7 @@ export function CommandPalette({ modalProps }) { setQuery(e)} - style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0" }} + style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0", paddingLeft: "0.9rem" }} placeholder="Search the Command Palette" />
@@ -94,6 +94,7 @@ export function CommandPalette({ modalProps }) { key={action.id} className={cl("option", { "key-hover": index === focusedIndex })} onClick={() => handleButtonClick(action.id, index)} + onMouseEnter={() => setFocusedIndex(index)} // Add this line > {action.label} diff --git a/src/plugins/commandPalette/components/MultipleChoice.tsx b/src/plugins/commandPalette/components/MultipleChoice.tsx index 92e5761a26..8f5cf54bad 100644 --- a/src/plugins/commandPalette/components/MultipleChoice.tsx +++ b/src/plugins/commandPalette/components/MultipleChoice.tsx @@ -87,7 +87,7 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice setQuery(e)} - style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0" }} + style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0", paddingLeft: "0.9rem" }} placeholder="Search the Command Palette" />
@@ -96,6 +96,7 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice key={action.id} className={cl("option", { "key-hover": index === focusedIndex })} onClick={() => handleButtonClick(action.id, index)} + onMouseEnter={() => setFocusedIndex(index)} > {action.label} diff --git a/src/plugins/commandPalette/components/styles.css b/src/plugins/commandPalette/components/styles.css index 4893e38f9d..27f1ac1664 100644 --- a/src/plugins/commandPalette/components/styles.css +++ b/src/plugins/commandPalette/components/styles.css @@ -3,6 +3,7 @@ .vc-command-palette-root { border-radius: 10px; overflow: hidden; + background-color: var(--background-tertiary) } .vc-command-palette-option { @@ -16,16 +17,18 @@ .vc-command-palette-key-hover { padding: 5px; - background-color: var(--background-tertiary); + background-color: var(--background-modifier-selected); + border-radius: 3px; font-family: var(--font-display); color: var(--interactive-hover); - border: 1.5px solid var(--interactive-hover); padding-left: 0.8rem; } .vc-command-palette-option-container{ display: grid; gap: 2px; + margin-left: 0.8rem; + margin-right: 0.8rem; } .vc-command-palette-textinfo { From 45af1d561361af7c9202a52e4f115a5ed913a348 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 4 Feb 2024 13:39:01 +0000 Subject: [PATCH 07/10] feat: added hotkey swapping --- .../components/CommandPalette.tsx | 5 +- .../components/MultipleChoice.tsx | 2 +- .../commandPalette/components/styles.css | 59 +++++++ src/plugins/commandPalette/index.ts | 43 ----- src/plugins/commandPalette/index.tsx | 149 ++++++++++++++++++ 5 files changed, 212 insertions(+), 46 deletions(-) delete mode 100644 src/plugins/commandPalette/index.ts create mode 100644 src/plugins/commandPalette/index.tsx diff --git a/src/plugins/commandPalette/components/CommandPalette.tsx b/src/plugins/commandPalette/components/CommandPalette.tsx index 44f3792726..dd9b0243ad 100644 --- a/src/plugins/commandPalette/components/CommandPalette.tsx +++ b/src/plugins/commandPalette/components/CommandPalette.tsx @@ -11,12 +11,13 @@ const logger = new Logger("CommandPalette", "#e5c890"); export function CommandPalette({ modalProps }) { const cl = classNameFactory("vc-command-palette-"); - const [queryEh, setQuery] = useState(""); const [focusedIndex, setFocusedIndex] = useState(null); const [startIndex, setStartIndex] = useState(0); const sortedActions = actions.slice().sort((a, b) => a.label.localeCompare(b.label)); + const [queryEh, setQuery] = useState(""); + const filteredActions = sortedActions.filter( (action) => action.label.toLowerCase().includes(queryEh.toLowerCase()) ); @@ -94,7 +95,7 @@ export function CommandPalette({ modalProps }) { key={action.id} className={cl("option", { "key-hover": index === focusedIndex })} onClick={() => handleButtonClick(action.id, index)} - onMouseEnter={() => setFocusedIndex(index)} // Add this line + onMouseMove={() => setFocusedIndex(index)} > {action.label} diff --git a/src/plugins/commandPalette/components/MultipleChoice.tsx b/src/plugins/commandPalette/components/MultipleChoice.tsx index 8f5cf54bad..b7f2c091a7 100644 --- a/src/plugins/commandPalette/components/MultipleChoice.tsx +++ b/src/plugins/commandPalette/components/MultipleChoice.tsx @@ -96,7 +96,7 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice key={action.id} className={cl("option", { "key-hover": index === focusedIndex })} onClick={() => handleButtonClick(action.id, index)} - onMouseEnter={() => setFocusedIndex(index)} + onMouseMove={() => setFocusedIndex(index)} > {action.label} diff --git a/src/plugins/commandPalette/components/styles.css b/src/plugins/commandPalette/components/styles.css index 27f1ac1664..d53818e734 100644 --- a/src/plugins/commandPalette/components/styles.css +++ b/src/plugins/commandPalette/components/styles.css @@ -43,3 +43,62 @@ background-color: var(--input-background); width: 30wh; } + +.vc-command-palette-key-recorder-container { + position: relative; + display: block; + cursor: pointer; + height: 40px; + width: 20rem; +} + +.vc-command-palette-key-recorder { + display: flex; + align-items: center; + border-radius: 3px; + background-color: hsl(var(--black-500-hsl)/10%); + color: var(--header-primary); + line-height: 22px; + font-weight: 600; + padding: 10px 0 10px 10px; + text-overflow: ellipsis; + overflow: hidden; + border: 1px solid; + border-color: hsl(var(--black-500-hsl)/30%); + transition: border.15s ease; + user-select: none; + font-family: var(--font-primary); +} + +.vc-command-palette-recording { + animation: shadowPulse_b16790 1s ease-in infinite; + box-shadow: 0 0 6px hsl(var(--red-400-hsl)/.3); + border-color: hsl(var(--red-400-hsl)/.3); + color: var(--status-danger); +} + +.vc-command-palette-key-recorder:hover { + border-color: hsl(var(--red-400-hsl)/30%); +} + +.vc-command-palette-recording-button { + color: var(--status-danger) !important; + background-color: hsl(var(--red-400-hsl)/.1) !important; + opacity: 1; + transition: opacity.2s ease-in-out,transform.2s ease-in-out; +} + +.vc-command-palette-key-recorder-button { + position: absolute; + right: 1rem; + height: 30px; + width: 128px; + color: var(--white-500); + background-color: var(--button-secondary-background); + border-radius: 10px; + transition: background-color 0.15s ease; +} + +.vc-command-palette-key-recorder-button:hover { + background-color: var(--button-secondary-background-hover) +} diff --git a/src/plugins/commandPalette/index.ts b/src/plugins/commandPalette/index.ts deleted file mode 100644 index 81e508e615..0000000000 --- a/src/plugins/commandPalette/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import definePlugin from "@utils/types"; -import { openCommandPalette } from "./components/CommandPalette"; -import { closeAllModals } from "@utils/modal"; -import { SettingsRouter } from "@webpack/common"; -import { registerAction } from "./commands"; -import { Devs } from "@utils/constants"; - - -export default definePlugin({ - name: "CommandPalette", - description: "Allows you to navigate the UI with a keyboard.", - authors: [Devs.Ethan], - - start() { - document.addEventListener("keydown", this.event); - - if (IS_DEV) { - registerAction({ - id: 'openDevSettings', - label: 'Open Dev tab', - callback: () => SettingsRouter.open("VencordPatchHelper") - }); - } - }, - - stop() { - document.removeEventListener("keydown", this.event); - }, - - event(e: KeyboardEvent) { - const { ctrlKey, shiftKey, key } = e; - - if (!ctrlKey || !shiftKey || key !== "P") return; - - closeAllModals(); - - if (document.querySelector(".vc-command-palette-root")) { // Allows for a toggle - return; - } - - openCommandPalette(); - } -}); diff --git a/src/plugins/commandPalette/index.tsx b/src/plugins/commandPalette/index.tsx new file mode 100644 index 0000000000..c18d60faf7 --- /dev/null +++ b/src/plugins/commandPalette/index.tsx @@ -0,0 +1,149 @@ +import definePlugin, { OptionType } from "@utils/types"; +import { openCommandPalette } from "./components/CommandPalette"; +import { closeAllModals } from "@utils/modal"; +import { Button, SettingsRouter } from "@webpack/common"; +import { registerAction } from "./commands"; +import { Devs } from "@utils/constants"; +import { definePluginSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; + +const cl = classNameFactory("vc-command-palette-"); +let recording: boolean = false; + +const settings = definePluginSettings({ + hotkey: { + description: "The hotkey to open the command palette.", + type: OptionType.COMPONENT, + default: ["Control", "Shift", "P"], + component: () => ( + <> +
+
+ {settings.store.hotkey.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" + ")} + +
+
+ + ) + } +}); + +function recordKeybind() { + let keys: Set = new Set(); + let keyLists: string[][] = []; + + const updateComponentText = () => { + const button = document.querySelector(`.${cl("key-recorder-button")}`); + const div = document.querySelector(`.${cl("key-recorder")}`); + + if (button) { + button.textContent = recording ? "Recording..." : "Record keybind"; + button.classList.toggle(cl("recording-button"), recording); + } + + if (div) { + div.classList.toggle(cl("recording"), recording); + } + }; + + recording = true; + updateComponentText(); + + const updateKeys = () => { + + + if (keys.size === 0 || !document.querySelector(`.${cl("key-recorder-button")}`)) { + const longestArray = keyLists.reduce((a, b) => a.length > b.length ? a : b); + + if (longestArray.length > 0) { + settings.store.hotkey = longestArray.map((key) => key.toLowerCase()); + } + + recording = false; + updateComponentText(); + + document.removeEventListener("keydown", keydownListener); + document.removeEventListener("keyup", keyupListener); + } + + keyLists.push(Array.from(keys)); + }; + + const keydownListener = (e: KeyboardEvent) => { + const { key } = e; + + if (!keys.has(key)) { + keys.add(key); + } + + updateKeys(); + }; + + const keyupListener = (e: KeyboardEvent) => { + keys.delete(e.key); + updateKeys(); + }; + + document.addEventListener("keydown", keydownListener); + document.addEventListener("keyup", keyupListener); +} + + +export default definePlugin({ + name: "CommandPalette", + description: "Allows you to navigate the UI with a keyboard.", + authors: [Devs.Ethan], + settings, + + start() { + document.addEventListener("keydown", this.event); + + if (IS_DEV) { + registerAction({ + id: 'openDevSettings', + label: 'Open Dev tab', + callback: () => SettingsRouter.open("VencordPatchHelper") + }); + } + }, + + stop() { + document.removeEventListener("keydown", this.event); + }, + + + event(e: KeyboardEvent) { + + enum Modifiers { + control = "ctrlKey", + shift = "shiftKey", + alt = "altKey", + meta = "metaKey" + } + + const { hotkey } = settings.store; + const pressedKey = e.key.toLowerCase(); + + if (recording) return; + + for (let i = 0; i < hotkey.length; i++) { + const lowercasedRequiredKey = hotkey[i].toLowerCase(); + + if (lowercasedRequiredKey in Modifiers && !e[Modifiers[lowercasedRequiredKey]]) { + return; + } + + if (!(lowercasedRequiredKey in Modifiers) && pressedKey !== lowercasedRequiredKey) { + return; + } + } + + closeAllModals(); + + if (document.querySelector(`.${cl("root")}`)) return; + + openCommandPalette(); + } +}); From a87fa36b50c39a3048f2b2523e394c62da87539c Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 4 Feb 2024 13:59:09 +0000 Subject: [PATCH 08/10] feat: option to allow mouse control --- .../components/CommandPalette.tsx | 19 ++++++++++++++++--- .../components/MultipleChoice.tsx | 19 ++++++++++++++++--- src/plugins/commandPalette/index.tsx | 7 ++++++- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/plugins/commandPalette/components/CommandPalette.tsx b/src/plugins/commandPalette/components/CommandPalette.tsx index dd9b0243ad..734988447f 100644 --- a/src/plugins/commandPalette/components/CommandPalette.tsx +++ b/src/plugins/commandPalette/components/CommandPalette.tsx @@ -4,6 +4,7 @@ import { closeAllModals, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { React, TextInput, useState } from "@webpack/common"; import { useEffect } from "@webpack/common"; import { actions } from "../commands"; +import { settings } from ".."; import "./styles.css"; @@ -14,6 +15,8 @@ export function CommandPalette({ modalProps }) { const [focusedIndex, setFocusedIndex] = useState(null); const [startIndex, setStartIndex] = useState(0); + const allowMouse = settings.store.allowMouseControl; + const sortedActions = actions.slice().sort((a, b) => a.label.localeCompare(b.label)); const [queryEh, setQuery] = useState(""); @@ -26,6 +29,16 @@ export function CommandPalette({ modalProps }) { const totalActions = filteredActions.length; + const handleWheel = (e: React.WheelEvent) => { + if (allowMouse && filteredActions.length > 20) { + if (e.deltaY > 0) { + setStartIndex((prev) => Math.min(prev + 2, filteredActions.length - 20)); + } else { + setStartIndex((prev) => Math.max(prev - 2, 0)); + } + } + }; + const handleButtonClick = (actionId: string, index: number) => { const selectedAction = filteredActions.find((action) => action.id === actionId); @@ -81,7 +94,7 @@ export function CommandPalette({ modalProps }) { }, [queryEh]); return ( - +
handleButtonClick(action.id, index)} - onMouseMove={() => setFocusedIndex(index)} + onClick={() => { if (allowMouse) handleButtonClick(action.id, index); }} + onMouseMove={() => { if (allowMouse) setFocusedIndex(index); }} > {action.label} diff --git a/src/plugins/commandPalette/components/MultipleChoice.tsx b/src/plugins/commandPalette/components/MultipleChoice.tsx index b7f2c091a7..8f4bbd9db1 100644 --- a/src/plugins/commandPalette/components/MultipleChoice.tsx +++ b/src/plugins/commandPalette/components/MultipleChoice.tsx @@ -6,6 +6,7 @@ import { useEffect } from "@webpack/common"; import { ButtonAction } from "../commands"; import "./styles.css"; +import { settings } from ".."; interface MultipleChoiceProps { modalProps: ModalProps; @@ -19,6 +20,8 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice const [focusedIndex, setFocusedIndex] = useState(null); const [startIndex, setStartIndex] = useState(0); + const allowMouse = settings.store.allowMouseControl; + const sortedActions = choices.slice().sort((a, b) => a.label.localeCompare(b.label)); const filteredActions = sortedActions.filter( @@ -76,13 +79,23 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice } }; + const handleWheel = (e: React.WheelEvent) => { + if (allowMouse && filteredActions.length > 20) { + if (e.deltaY > 0) { + setStartIndex((prev) => Math.min(prev + 2, filteredActions.length - 20)); + } else { + setStartIndex((prev) => Math.max(prev - 2, 0)); + } + } + }; + useEffect(() => { setFocusedIndex(0); setStartIndex(0); }, [queryEh]); return ( - +
handleButtonClick(action.id, index)} - onMouseMove={() => setFocusedIndex(index)} + onClick={() => { if (allowMouse) handleButtonClick(action.id, index); }} + onMouseMove={() => { if (allowMouse) setFocusedIndex(index); }} > {action.label} diff --git a/src/plugins/commandPalette/index.tsx b/src/plugins/commandPalette/index.tsx index c18d60faf7..0b929769e5 100644 --- a/src/plugins/commandPalette/index.tsx +++ b/src/plugins/commandPalette/index.tsx @@ -10,7 +10,7 @@ import { classNameFactory } from "@api/Styles"; const cl = classNameFactory("vc-command-palette-"); let recording: boolean = false; -const settings = definePluginSettings({ +export const settings = definePluginSettings({ hotkey: { description: "The hotkey to open the command palette.", type: OptionType.COMPONENT, @@ -27,6 +27,11 @@ const settings = definePluginSettings({
) + }, + allowMouseControl: { + description: "Allow the mouse to control the command palette.", + type: OptionType.BOOLEAN, + default: false } }); From dbeebd7e1789307a5fac642d9666699c66eee29b Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 4 Feb 2024 14:26:10 +0000 Subject: [PATCH 09/10] feat: allow for command registrars --- src/plugins/commandPalette/commands.tsx | 31 ++++++++++--------- .../components/CommandPalette.tsx | 2 ++ .../components/MultipleChoice.tsx | 2 ++ .../commandPalette/components/styles.css | 8 +++++ src/plugins/commandPalette/index.tsx | 5 +-- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/plugins/commandPalette/commands.tsx b/src/plugins/commandPalette/commands.tsx index 43cb2ec278..518424e105 100644 --- a/src/plugins/commandPalette/commands.tsx +++ b/src/plugins/commandPalette/commands.tsx @@ -17,19 +17,20 @@ export interface ButtonAction { id: string; label: string; callback?: () => void; + registrar?: string; } export let actions: ButtonAction[] = [ - { id: 'openVencordSettings', label: 'Open Vencord tab', callback: async () => await SettingsRouter.open("VencordSettings") }, - { id: 'openPluginSettings', label: 'Open Plugin tab', callback: () => SettingsRouter.open("VencordPlugins") }, - { id: 'openThemesSettings', label: 'Open Themes tab', callback: () => SettingsRouter.open("VencordThemes") }, - { id: 'openUpdaterSettings', label: 'Open Updater tab', callback: () => SettingsRouter.open("VencordUpdater") }, - { id: 'openVencordCloudSettings', label: 'Open Cloud tab', callback: () => SettingsRouter.open("VencordCloud") }, - { id: 'openBackupSettings', label: 'Open Backup & Restore tab', callback: () => SettingsRouter.open("VencordSettingsSync") }, - { id: 'restartClient', label: 'Restart Client', callback: () => relaunch() }, - { id: 'openQuickCSSFile', label: 'Open Quick CSS File', callback: () => VencordNative.quickCss.openEditor() }, - { id: 'openSettingsFolder', label: 'Open Settings Folder', callback: async () => showItemInFolder(await VencordNative.settings.getSettingsDir()) }, - { id: 'openInGithub', label: 'Open in Github', callback: async () => VencordNative.native.openExternal(await getRepo()) }, + { id: 'openVencordSettings', label: 'Open Vencord tab', callback: async () => await SettingsRouter.open("VencordSettings"), registrar: "Vencord" }, + { id: 'openPluginSettings', label: 'Open Plugin tab', callback: () => SettingsRouter.open("VencordPlugins"), registrar: "Vencord" }, + { id: 'openThemesSettings', label: 'Open Themes tab', callback: () => SettingsRouter.open("VencordThemes"), registrar: "Vencord" }, + { id: 'openUpdaterSettings', label: 'Open Updater tab', callback: () => SettingsRouter.open("VencordUpdater"), registrar: "Vencord" }, + { id: 'openVencordCloudSettings', label: 'Open Cloud tab', callback: () => SettingsRouter.open("VencordCloud"), registrar: "Vencord" }, + { id: 'openBackupSettings', label: 'Open Backup & Restore tab', callback: () => SettingsRouter.open("VencordSettingsSync"), registrar: "Vencord" }, + { id: 'restartClient', label: 'Restart Client', callback: () => relaunch(), registrar: "Vencord" }, + { id: 'openQuickCSSFile', label: 'Open Quick CSS File', callback: () => VencordNative.quickCss.openEditor(), registrar: "Vencord" }, + { id: 'openSettingsFolder', label: 'Open Settings Folder', callback: async () => showItemInFolder(await VencordNative.settings.getSettingsDir()), registrar: "Vencord" }, + { id: 'openInGithub', label: 'Open in Github', callback: async () => VencordNative.native.openExternal(await getRepo()), registrar: "Vencord" }, { id: 'openInBrowser', label: 'Open in Browser', callback: async () => { @@ -49,7 +50,7 @@ export let actions: ButtonAction[] = [ } }); } - } + }, registrar: "Vencord" }, { @@ -74,7 +75,7 @@ export let actions: ButtonAction[] = [ if (choice && enabled) { return togglePlugin(choice, enabled.id === 'enable'); } - } + }, registrar: "Vencord" }, { @@ -105,7 +106,7 @@ export let actions: ButtonAction[] = [ } }); } - } + }, registrar: "Vencord" }, { @@ -132,7 +133,7 @@ export let actions: ButtonAction[] = [ } }); } - } + }, registrar: "Vencord" }, { @@ -152,7 +153,7 @@ export let actions: ButtonAction[] = [ if (choice) { NavigationRouter.transitionToGuild(choice.id); } - } + }, registrar: "Vencord" } ]; diff --git a/src/plugins/commandPalette/components/CommandPalette.tsx b/src/plugins/commandPalette/components/CommandPalette.tsx index 734988447f..793024c5bf 100644 --- a/src/plugins/commandPalette/components/CommandPalette.tsx +++ b/src/plugins/commandPalette/components/CommandPalette.tsx @@ -109,8 +109,10 @@ export function CommandPalette({ modalProps }) { className={cl("option", { "key-hover": index === focusedIndex })} onClick={() => { if (allowMouse) handleButtonClick(action.id, index); }} onMouseMove={() => { if (allowMouse) setFocusedIndex(index); }} + style={allowMouse ? { cursor: "pointer" } : { cursor: "default" }} > {action.label} + {action.registrar && {action.registrar}} ))}
diff --git a/src/plugins/commandPalette/components/MultipleChoice.tsx b/src/plugins/commandPalette/components/MultipleChoice.tsx index 8f4bbd9db1..dcb9026ead 100644 --- a/src/plugins/commandPalette/components/MultipleChoice.tsx +++ b/src/plugins/commandPalette/components/MultipleChoice.tsx @@ -110,8 +110,10 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice className={cl("option", { "key-hover": index === focusedIndex })} onClick={() => { if (allowMouse) handleButtonClick(action.id, index); }} onMouseMove={() => { if (allowMouse) setFocusedIndex(index); }} + style={allowMouse ? { cursor: "pointer" } : { cursor: "default" }} > {action.label} + {action.registrar && {action.registrar}} ))}
diff --git a/src/plugins/commandPalette/components/styles.css b/src/plugins/commandPalette/components/styles.css index d53818e734..bd4357ef6a 100644 --- a/src/plugins/commandPalette/components/styles.css +++ b/src/plugins/commandPalette/components/styles.css @@ -44,6 +44,14 @@ width: 30wh; } +.vc-command-palette-registrar { + position: absolute; + color: var(--interactive-normal); + white-space: nowrap; + text-overflow: ellipsis; + right: 1.6rem; +} + .vc-command-palette-key-recorder-container { position: relative; display: block; diff --git a/src/plugins/commandPalette/index.tsx b/src/plugins/commandPalette/index.tsx index 0b929769e5..655ab14dca 100644 --- a/src/plugins/commandPalette/index.tsx +++ b/src/plugins/commandPalette/index.tsx @@ -31,7 +31,7 @@ export const settings = definePluginSettings({ allowMouseControl: { description: "Allow the mouse to control the command palette.", type: OptionType.BOOLEAN, - default: false + default: true } }); @@ -109,7 +109,8 @@ export default definePlugin({ registerAction({ id: 'openDevSettings', label: 'Open Dev tab', - callback: () => SettingsRouter.open("VencordPatchHelper") + callback: () => SettingsRouter.open("VencordPatchHelper"), + registrar: "Vencord" }); } }, From 6bf74518b772e12e87bad7b83766a0ef94242a03 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 8 Feb 2024 19:03:07 +0000 Subject: [PATCH 10/10] resolved requests --- .../commandPalette/COMMAND_PALETTE_DOC.md | 0 src/plugins/commandPalette/index.tsx | 130 ++++++++---------- 2 files changed, 56 insertions(+), 74 deletions(-) rename docs/3_COMMAND_PALETTE.md => src/plugins/commandPalette/COMMAND_PALETTE_DOC.md (100%) diff --git a/docs/3_COMMAND_PALETTE.md b/src/plugins/commandPalette/COMMAND_PALETTE_DOC.md similarity index 100% rename from docs/3_COMMAND_PALETTE.md rename to src/plugins/commandPalette/COMMAND_PALETTE_DOC.md diff --git a/src/plugins/commandPalette/index.tsx b/src/plugins/commandPalette/index.tsx index 655ab14dca..6b675f9164 100644 --- a/src/plugins/commandPalette/index.tsx +++ b/src/plugins/commandPalette/index.tsx @@ -1,32 +1,74 @@ import definePlugin, { OptionType } from "@utils/types"; import { openCommandPalette } from "./components/CommandPalette"; import { closeAllModals } from "@utils/modal"; -import { Button, SettingsRouter } from "@webpack/common"; +import { Button, SettingsRouter, useState } from "@webpack/common"; import { registerAction } from "./commands"; import { Devs } from "@utils/constants"; import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; const cl = classNameFactory("vc-command-palette-"); -let recording: boolean = false; +let isRecordingGlobal: boolean = false; export const settings = definePluginSettings({ hotkey: { description: "The hotkey to open the command palette.", type: OptionType.COMPONENT, default: ["Control", "Shift", "P"], - component: () => ( - <> -
-
- {settings.store.hotkey.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" + ")} - + component: () => { + const [isRecording, setIsRecording] = useState(false); + + const recordKeybind = (setIsRecording: (value: boolean) => void) => { + let keys: Set = new Set(); + let keyLists: string[][] = []; + + setIsRecording(true); + isRecordingGlobal = true; + + const updateKeys = () => { + if (keys.size === 0 || !document.querySelector(`.${cl("key-recorder-button")}`)) { + const longestArray = keyLists.reduce((a, b) => a.length > b.length ? a : b); + if (longestArray.length > 0) { + settings.store.hotkey = longestArray.map((key) => key.toLowerCase()); + } + setIsRecording(false); + isRecordingGlobal = false; + document.removeEventListener("keydown", keydownListener); + document.removeEventListener("keyup", keyupListener); + } + keyLists.push(Array.from(keys)); + }; + + const keydownListener = (e: KeyboardEvent) => { + const { key } = e; + if (!keys.has(key)) { + keys.add(key); + } + updateKeys(); + }; + + const keyupListener = (e: KeyboardEvent) => { + keys.delete(e.key); + updateKeys(); + }; + + document.addEventListener("keydown", keydownListener); + document.addEventListener("keyup", keyupListener); + }; + + return ( + <> +
recordKeybind(setIsRecording)}> +
+ {settings.store.hotkey.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" + ")} + +
-
- - ) + + ); + } }, allowMouseControl: { description: "Allow the mouse to control the command palette.", @@ -35,66 +77,6 @@ export const settings = definePluginSettings({ } }); -function recordKeybind() { - let keys: Set = new Set(); - let keyLists: string[][] = []; - - const updateComponentText = () => { - const button = document.querySelector(`.${cl("key-recorder-button")}`); - const div = document.querySelector(`.${cl("key-recorder")}`); - - if (button) { - button.textContent = recording ? "Recording..." : "Record keybind"; - button.classList.toggle(cl("recording-button"), recording); - } - - if (div) { - div.classList.toggle(cl("recording"), recording); - } - }; - - recording = true; - updateComponentText(); - - const updateKeys = () => { - - - if (keys.size === 0 || !document.querySelector(`.${cl("key-recorder-button")}`)) { - const longestArray = keyLists.reduce((a, b) => a.length > b.length ? a : b); - - if (longestArray.length > 0) { - settings.store.hotkey = longestArray.map((key) => key.toLowerCase()); - } - - recording = false; - updateComponentText(); - - document.removeEventListener("keydown", keydownListener); - document.removeEventListener("keyup", keyupListener); - } - - keyLists.push(Array.from(keys)); - }; - - const keydownListener = (e: KeyboardEvent) => { - const { key } = e; - - if (!keys.has(key)) { - keys.add(key); - } - - updateKeys(); - }; - - const keyupListener = (e: KeyboardEvent) => { - keys.delete(e.key); - updateKeys(); - }; - - document.addEventListener("keydown", keydownListener); - document.addEventListener("keyup", keyupListener); -} - export default definePlugin({ name: "CommandPalette", @@ -132,7 +114,7 @@ export default definePlugin({ const { hotkey } = settings.store; const pressedKey = e.key.toLowerCase(); - if (recording) return; + if (isRecordingGlobal) return; for (let i = 0; i < hotkey.length; i++) { const lowercasedRequiredKey = hotkey[i].toLowerCase();