This commit is contained in:
thororen1234 2024-07-18 00:53:55 -04:00
parent 7809f5b67c
commit 3c4d217312
108 changed files with 7134 additions and 38 deletions

View 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);
}

View 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} />);

View 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}
/>
));
});
}

View 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}
/>
));
});
}

View 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);
}

View 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();
}
});