mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-22 12:57:01 -04:00
Stuff
This commit is contained in:
parent
7809f5b67c
commit
3c4d217312
108 changed files with 7134 additions and 38 deletions
195
src/equicordplugins/commandPalette/commands.tsx
Normal file
195
src/equicordplugins/commandPalette/commands.tsx
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { relaunch, showItemInFolder } from "@utils/native";
|
||||
import { checkForUpdates, getRepo } from "@utils/updater";
|
||||
import { Clipboard, GuildStore, NavigationRouter, SettingsRouter, Toasts } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
import gitRemote from "~git-remote";
|
||||
import Plugins from "~plugins";
|
||||
|
||||
import { openMultipleChoice } from "./components/MultipleChoice";
|
||||
import { openSimpleTextInput } from "./components/TextInput";
|
||||
|
||||
export interface ButtonAction {
|
||||
id: string;
|
||||
label: string;
|
||||
callback?: () => void;
|
||||
registrar?: string;
|
||||
}
|
||||
|
||||
export const actions: ButtonAction[] = [
|
||||
{ id: "openSuncordSettings", label: "Open Suncord tab", callback: async () => await SettingsRouter.open("SuncordSettings"), registrar: "Suncord" },
|
||||
{ id: "openPluginSettings", label: "Open Plugin tab", callback: () => SettingsRouter.open("SuncordPlugins"), registrar: "Suncord" },
|
||||
{ id: "openThemesSettings", label: "Open Themes tab", callback: () => SettingsRouter.open("SuncordThemes"), registrar: "Suncord" },
|
||||
{ id: "openUpdaterSettings", label: "Open Updater tab", callback: () => SettingsRouter.open("SuncordUpdater"), registrar: "Suncord" },
|
||||
{ id: "openSuncordCloudSettings", label: "Open Cloud tab", callback: () => SettingsRouter.open("SuncordCloud"), registrar: "Suncord" },
|
||||
{ id: "openBackupSettings", label: "Open Backup & Restore tab", callback: () => SettingsRouter.open("SuncordSettingsSync"), registrar: "Suncord" },
|
||||
{ id: "restartClient", label: "Restart Client", callback: () => relaunch(), registrar: "Suncord" },
|
||||
{ id: "openQuickCSSFile", label: "Open Quick CSS File", callback: () => VencordNative.quickCss.openEditor(), registrar: "Suncord" },
|
||||
{ id: "openSettingsFolder", label: "Open Settings Folder", callback: async () => showItemInFolder(await VencordNative.settings.getSettingsDir()), registrar: "Suncord" },
|
||||
{ id: "openInGithub", label: "Open in Github", callback: async () => VencordNative.native.openExternal(await getRepo()), registrar: "Suncord" },
|
||||
|
||||
{
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
}, registrar: "Suncord"
|
||||
},
|
||||
|
||||
{
|
||||
id: "togglePlugin", label: "Toggle Plugin", callback: async () => {
|
||||
const plugins = Object.keys(Plugins);
|
||||
const 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 && enabled) {
|
||||
return togglePlugin(choice, enabled.id === "enable");
|
||||
}
|
||||
}, registrar: "Suncord"
|
||||
},
|
||||
|
||||
{
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
}, registrar: "Suncord"
|
||||
},
|
||||
|
||||
{
|
||||
id: "copyGitInfo", label: "Copy Git Info", callback: async () => {
|
||||
Clipboard.copy(`gitHash: ${gitHash}\ngitRemote: ${gitRemote}`);
|
||||
|
||||
Toasts.show({
|
||||
message: "Copied git info to clipboard!",
|
||||
type: Toasts.Type.SUCCESS,
|
||||
id: Toasts.genId(),
|
||||
options: {
|
||||
position: Toasts.Position.BOTTOM
|
||||
}
|
||||
});
|
||||
}, registrar: "Suncord"
|
||||
},
|
||||
|
||||
{
|
||||
id: "checkForUpdates", label: "Check for Updates", callback: async () => {
|
||||
const isOutdated = await checkForUpdates();
|
||||
|
||||
if (isOutdated) {
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Suncord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick() {
|
||||
SettingsRouter.open("SuncordUpdater");
|
||||
}
|
||||
}), 10_000);
|
||||
} else {
|
||||
Toasts.show({
|
||||
message: "No updates available",
|
||||
type: Toasts.Type.MESSAGE,
|
||||
id: Toasts.genId(),
|
||||
options: {
|
||||
position: Toasts.Position.BOTTOM
|
||||
}
|
||||
});
|
||||
}
|
||||
}, registrar: "Suncord"
|
||||
},
|
||||
|
||||
{
|
||||
id: "navToServer", label: "Navigate to Server", callback: async () => {
|
||||
const allServers = Object.values(GuildStore.getGuilds());
|
||||
const 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);
|
||||
}
|
||||
}, registrar: "Suncord"
|
||||
}
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
130
src/equicordplugins/commandPalette/components/CommandPalette.tsx
Normal file
130
src/equicordplugins/commandPalette/components/CommandPalette.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { closeAllModals, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { settings } from "..";
|
||||
import { actions } from "../commands";
|
||||
|
||||
const logger = new Logger("CommandPalette", "#e5c890");
|
||||
|
||||
export function CommandPalette({ modalProps }) {
|
||||
const cl = classNameFactory("vc-command-palette-");
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(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("");
|
||||
|
||||
const filteredActions = sortedActions.filter(
|
||||
action => action.label.toLowerCase().includes(queryEh.toLowerCase())
|
||||
);
|
||||
|
||||
const visibleActions = filteredActions.slice(startIndex, startIndex + 20);
|
||||
|
||||
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);
|
||||
|
||||
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 (
|
||||
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.MEDIUM} onKeyDown={handleKeyDown} onWheel={handleWheel}>
|
||||
<div>
|
||||
<TextInput
|
||||
value={queryEh}
|
||||
onChange={e => setQuery(e)}
|
||||
style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0", paddingLeft: "0.9rem" }}
|
||||
placeholder="Search the Command Palette"
|
||||
/>
|
||||
<div className={cl("option-container")}>
|
||||
{visibleActions.map((action, index) => (
|
||||
<button
|
||||
key={action.id}
|
||||
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 && <span className={cl("registrar")}>{action.registrar}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export const openCommandPalette = () => openModal(modalProps => <CommandPalette modalProps={modalProps} />);
|
144
src/equicordplugins/commandPalette/components/MultipleChoice.tsx
Normal file
144
src/equicordplugins/commandPalette/components/MultipleChoice.tsx
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { settings } from "..";
|
||||
import { ButtonAction } from "../commands";
|
||||
|
||||
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<number | null>(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(
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
// @ts-ignore
|
||||
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.MEDIUM} onKeyDown={handleKeyDown} onWheel={handleWheel}>
|
||||
<div>
|
||||
<TextInput
|
||||
value={queryEh}
|
||||
onChange={e => setQuery(e)}
|
||||
style={{ width: "100%", borderBottomLeftRadius: "0", borderBottomRightRadius: "0", paddingLeft: "0.9rem" }}
|
||||
placeholder="Search the Command Palette"
|
||||
/>
|
||||
<div className={cl("option-container")}>
|
||||
{visibleActions.map((action, index) => (
|
||||
<button
|
||||
key={action.id}
|
||||
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 && <span className={cl("registrar")}>{action.registrar}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export function openMultipleChoice(choices: ButtonAction[]): Promise<ButtonAction> {
|
||||
return new Promise(resolve => {
|
||||
openModal(modalProps => (
|
||||
<MultipleChoice
|
||||
modalProps={modalProps}
|
||||
onSelect={selectedValue => {
|
||||
closeAllModals();
|
||||
resolve(selectedValue);
|
||||
}}
|
||||
choices={choices}
|
||||
/>
|
||||
));
|
||||
});
|
||||
}
|
63
src/equicordplugins/commandPalette/components/TextInput.tsx
Normal file
63
src/equicordplugins/commandPalette/components/TextInput.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { React, TextInput, useEffect, useState } from "@webpack/common";
|
||||
|
||||
interface SimpleTextInputProps {
|
||||
modalProps: ModalProps;
|
||||
onSelect: (inputValue: string) => void;
|
||||
placeholder?: string;
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export function SimpleTextInput({ modalProps, onSelect, placeholder, info }: SimpleTextInputProps) {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case "Enter":
|
||||
onSelect(inputValue);
|
||||
closeAllModals();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue("");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<ModalRoot className="vc-command-palette-simple-text" {...modalProps} size={ModalSize.DYNAMIC} onKeyDown={handleKeyDown}>
|
||||
<TextInput
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e as unknown as string)}
|
||||
style={{ width: "30vw", borderRadius: "5px" }}
|
||||
placeholder={placeholder ?? "Type and press Enter"}
|
||||
/>
|
||||
{info && <div className="vc-command-palette-textinfo">{info}</div>}
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function openSimpleTextInput(placeholder?: string, info?: string): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
openModal(modalProps => (
|
||||
<SimpleTextInput
|
||||
modalProps={modalProps}
|
||||
onSelect={inputValue => resolve(inputValue)}
|
||||
placeholder={placeholder}
|
||||
info={info}
|
||||
/>
|
||||
));
|
||||
});
|
||||
}
|
112
src/equicordplugins/commandPalette/components/style.css
Normal file
112
src/equicordplugins/commandPalette/components/style.css
Normal file
|
@ -0,0 +1,112 @@
|
|||
/* cl = vc-command-palette- */
|
||||
|
||||
.vc-command-palette-root {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background-color: var(--background-tertiary);
|
||||
}
|
||||
|
||||
.vc-command-palette-option {
|
||||
padding: 5px;
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--white-500);
|
||||
font-family: var(--font-display);
|
||||
text-align: left;
|
||||
padding-left: 0.8rem;
|
||||
}
|
||||
|
||||
.vc-command-palette-key-hover {
|
||||
padding: 5px;
|
||||
background-color: var(--background-modifier-selected);
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-display);
|
||||
color: 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 {
|
||||
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: 30vh;
|
||||
}
|
||||
|
||||
.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;
|
||||
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) / 30%);
|
||||
border-color: hsl(var(--red-400-hsl) / 30%);
|
||||
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) / 10%) !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);
|
||||
}
|
144
src/equicordplugins/commandPalette/index.tsx
Normal file
144
src/equicordplugins/commandPalette/index.tsx
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { closeAllModals } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { SettingsRouter, useState } from "@webpack/common";
|
||||
|
||||
import { registerAction } from "./commands";
|
||||
import { openCommandPalette } from "./components/CommandPalette";
|
||||
|
||||
const cl = classNameFactory("vc-command-palette-");
|
||||
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: () => {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
|
||||
const recordKeybind = (setIsRecording: (value: boolean) => void) => {
|
||||
const keys: Set<string> = new Set();
|
||||
const 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 (
|
||||
<>
|
||||
<div className={cl("key-recorder-container")} onClick={() => recordKeybind(setIsRecording)}>
|
||||
<div className={`${cl("key-recorder")} ${isRecording ? cl("recording") : ""}`}>
|
||||
{settings.store.hotkey.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" + ")}
|
||||
<button className={`${cl("key-recorder-button")} ${isRecording ? cl("recording-button") : ""}`} disabled={isRecording}>
|
||||
{isRecording ? "Recording..." : "Record keybind"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
allowMouseControl: {
|
||||
description: "Allow the mouse to control the command palette.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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("SuncordPatchHelper"),
|
||||
registrar: "Suncord"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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 (isRecordingGlobal) 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();
|
||||
}
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue