Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
09e919f0c6 | ||
|
eaf1af75bd | ||
|
7c514e4b1d | ||
|
1432baa28b | ||
|
f1f61195c3 | ||
|
8fefa2b716 | ||
|
2a0c30b66d | ||
|
97f8d4d515 | ||
|
2672dea8e3 | ||
|
63f5b0a663 | ||
|
e40ebacc5b | ||
|
e261c93563 | ||
|
df7357b357 | ||
|
2e6c5eacf7 | ||
|
c9fd404012 | ||
|
814302e272 | ||
|
72ba83924c | ||
|
9d742094cb | ||
|
38f3aac98d | ||
|
12ffb9d642 | ||
|
99391a4f0e | ||
|
6492908a62 | ||
|
676bc612d9 | ||
|
d8a5e43034 | ||
|
8ad710abca | ||
|
368cb7bc6b |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 18
|
- name: Use Node.js 18
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
11
package.json
11
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.1.5",
|
"version": "1.1.6",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@ -21,8 +21,8 @@
|
|||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||||
"inject": "node scripts/runInstaller.mjs",
|
"inject": "node scripts/runInstaller.mjs",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
||||||
"lint-styles": "stylelint \"src/**/*.css\"",
|
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||||
@ -65,7 +65,7 @@
|
|||||||
"type-fest": "^3.5.3",
|
"type-fest": "^3.5.3",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.13.4",
|
"packageManager": "pnpm@8.1.1",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||||
@ -92,6 +92,7 @@
|
|||||||
"sourceDir": "./dist/extension-v2-unpacked"
|
"sourceDir": "./dist/extension-v2-unpacked"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18",
|
||||||
|
"pnpm": ">=8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1153
pnpm-lock.yaml
generated
1153
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -80,7 +80,7 @@ await Promise.all([
|
|||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
sourcemap,
|
sourcemap,
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins,
|
globPlugins("discordDesktop"),
|
||||||
...commonOpts.plugins
|
...commonOpts.plugins
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
@ -114,7 +114,7 @@ await Promise.all([
|
|||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
sourcemap,
|
sourcemap,
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins,
|
globPlugins("vencordDesktop"),
|
||||||
...commonOpts.plugins
|
...commonOpts.plugins
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
|
@ -38,7 +38,7 @@ const commonOptions = {
|
|||||||
format: "iife",
|
format: "iife",
|
||||||
external: ["plugins", "git-hash", "/assets/*"],
|
external: ["plugins", "git-hash", "/assets/*"],
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins,
|
globPlugins("web"),
|
||||||
...commonOpts.plugins,
|
...commonOpts.plugins,
|
||||||
],
|
],
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
|
@ -48,9 +48,9 @@ export const makeAllPackagesExternalPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {(kind: "web" | "discordDesktop" | "vencordDesktop") => import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const globPlugins = {
|
export const globPlugins = kind => ({
|
||||||
name: "glob-plugins",
|
name: "glob-plugins",
|
||||||
setup: build => {
|
setup: build => {
|
||||||
const filter = /^~plugins$/;
|
const filter = /^~plugins$/;
|
||||||
@ -76,8 +76,10 @@ export const globPlugins = {
|
|||||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
|
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
|
||||||
const mod = fileBits.at(-2);
|
const mod = fileBits.at(-2);
|
||||||
if (mod === "dev" && !watch) continue;
|
if (mod === "dev" && !watch) continue;
|
||||||
if (mod === "web" && !isWeb) continue;
|
if (mod === "web" && kind === "discordDesktop") continue;
|
||||||
if (mod === "desktop" && isWeb) continue;
|
if (mod === "desktop" && kind === "web") continue;
|
||||||
|
if (mod === "discordDesktop" && kind !== "discordDesktop") continue;
|
||||||
|
if (mod === "vencordDesktop" && kind !== "vencordDesktop") continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
@ -93,7 +95,7 @@ export const globPlugins = {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
|
@ -35,7 +35,7 @@ interface PluginData {
|
|||||||
hasCommands: boolean;
|
hasCommands: boolean;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
enabledByDefault: boolean;
|
enabledByDefault: boolean;
|
||||||
target: "desktop" | "web" | "dev";
|
target: "discordDesktop" | "vencordDesktop" | "web" | "dev";
|
||||||
}
|
}
|
||||||
|
|
||||||
const devs = {} as Record<string, Dev>;
|
const devs = {} as Record<string, Dev>;
|
||||||
@ -150,7 +150,7 @@ async function parseFile(fileName: string) {
|
|||||||
const fileBits = fileName.split(".");
|
const fileBits = fileName.split(".");
|
||||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
|
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
|
||||||
const mod = fileBits.at(-2)!;
|
const mod = fileBits.at(-2)!;
|
||||||
if (!["web", "desktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
|
if (!["web", "discordDesktop", "vencordDesktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
|
||||||
data.target = mod as any;
|
data.target = mod as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,17 +30,44 @@ import "./webpack/patchWebpack";
|
|||||||
import { showNotification } from "./api/Notifications";
|
import { showNotification } from "./api/Notifications";
|
||||||
import { PlainSettings, Settings } from "./api/settings";
|
import { PlainSettings, Settings } from "./api/settings";
|
||||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||||
|
import { localStorage } from "./utils/localStorage";
|
||||||
|
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
|
||||||
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { SettingsRouter } from "./webpack/common";
|
import { SettingsRouter } from "./webpack/common";
|
||||||
|
|
||||||
export let Components: any;
|
export let Components: any;
|
||||||
|
|
||||||
|
async function syncSettings() {
|
||||||
|
if (
|
||||||
|
Settings.cloud.settingsSync && // if it's enabled
|
||||||
|
Settings.cloud.authenticated // if cloud integrations are enabled
|
||||||
|
) {
|
||||||
|
if (localStorage.Vencord_settingsDirty) {
|
||||||
|
await putCloudSettings();
|
||||||
|
delete localStorage.Vencord_settingsDirty;
|
||||||
|
} else if (await getCloudSettings(false)) { // if we synchronized something (false means no sync)
|
||||||
|
// we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
|
||||||
|
// potential notifications that might occur. getCloudSettings() will always send a notification regardless if
|
||||||
|
// there was an error to notify the user, but besides that we only want to show one notification instead of all
|
||||||
|
// of the possible ones it has (such as when your settings are newer).
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
||||||
|
color: "var(--green-360)",
|
||||||
|
onClick: () => window.DiscordNative.app.relaunch()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await onceReady;
|
await onceReady;
|
||||||
startAllPlugins();
|
startAllPlugins();
|
||||||
Components = await import("./components");
|
Components = await import("./components");
|
||||||
|
|
||||||
|
syncSettings();
|
||||||
|
|
||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
try {
|
try {
|
||||||
const isOutdated = await checkForUpdates();
|
const isOutdated = await checkForUpdates();
|
||||||
|
@ -16,9 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { debounce } from "@utils/debounce";
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
|
import { localStorage } from "@utils/localStorage";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/misc";
|
||||||
|
import { putCloudSettings } from "@utils/settingsSync";
|
||||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
@ -49,6 +52,13 @@ export interface Settings {
|
|||||||
useNative: "always" | "never" | "not-focused";
|
useNative: "always" | "never" | "not-focused";
|
||||||
logLimit: number;
|
logLimit: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cloud: {
|
||||||
|
authenticated: boolean;
|
||||||
|
url: string;
|
||||||
|
settingsSync: boolean;
|
||||||
|
settingsSyncVersion: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
@ -69,6 +79,13 @@ const DefaultSettings: Settings = {
|
|||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
useNative: "not-focused",
|
useNative: "not-focused",
|
||||||
logLimit: 50
|
logLimit: 50
|
||||||
|
},
|
||||||
|
|
||||||
|
cloud: {
|
||||||
|
authenticated: false,
|
||||||
|
url: "https://api.vencord.dev/",
|
||||||
|
settingsSync: false,
|
||||||
|
settingsSyncVersion: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,6 +97,13 @@ try {
|
|||||||
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveSettingsOnFrequentAction = debounce(async () => {
|
||||||
|
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
||||||
|
await putCloudSettings();
|
||||||
|
delete localStorage.Vencord_settingsDirty;
|
||||||
|
}
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
||||||
const subscriptions = new Set<SubscriptionCallback>();
|
const subscriptions = new Set<SubscriptionCallback>();
|
||||||
|
|
||||||
@ -142,6 +166,9 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And don't forget to persist the settings!
|
// And don't forget to persist the settings!
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
||||||
|
localStorage.Vencord_settingsDirty = true;
|
||||||
|
saveSettingsOnFrequentAction();
|
||||||
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
164
src/components/VencordSettings/CloudTab.tsx
Normal file
164
src/components/VencordSettings/CloudTab.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { showNotification } from "@api/Notifications";
|
||||||
|
import { Settings, useSettings } from "@api/settings";
|
||||||
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
|
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
||||||
|
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
function validateUrl(url: string) {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return "Invalid URL";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function eraseAllData() {
|
||||||
|
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: new Headers({
|
||||||
|
Authorization: await getCloudAuth()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Integrations",
|
||||||
|
body: `Could not erase all data (API returned ${res.status}), please contact support.`,
|
||||||
|
color: "var(--red-360)"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.cloud.authenticated = false;
|
||||||
|
await deauthorizeCloud();
|
||||||
|
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Integrations",
|
||||||
|
body: "Successfully erased all data.",
|
||||||
|
color: "var(--green-360)"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSyncSection() {
|
||||||
|
const { cloud } = useSettings(["cloud.authenticated", "cloud.settingsSync"]);
|
||||||
|
const sectionEnabled = cloud.authenticated && cloud.settingsSync;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
||||||
|
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
||||||
|
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
|
||||||
|
minimal effort.
|
||||||
|
</Forms.FormText>
|
||||||
|
<Switch
|
||||||
|
key="cloud-sync"
|
||||||
|
disabled={!cloud.authenticated}
|
||||||
|
value={cloud.settingsSync}
|
||||||
|
onChange={v => { cloud.settingsSync = v; }}
|
||||||
|
>
|
||||||
|
Settings Sync
|
||||||
|
</Switch>
|
||||||
|
<div className="vc-cloud-settings-sync-grid">
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={!sectionEnabled}
|
||||||
|
onClick={() => putCloudSettings()}
|
||||||
|
>Sync to Cloud</Button>
|
||||||
|
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
|
||||||
|
{({ onMouseLeave, onMouseEnter }) => (
|
||||||
|
<Button
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
disabled={!sectionEnabled}
|
||||||
|
onClick={() => getCloudSettings(true, true)}
|
||||||
|
>Sync from Cloud</Button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
disabled={!sectionEnabled}
|
||||||
|
onClick={() => deleteCloudSettings()}
|
||||||
|
>Delete Cloud Settings</Button>
|
||||||
|
</div>
|
||||||
|
</Forms.FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CloudTab() {
|
||||||
|
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
|
||||||
|
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
||||||
|
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
|
||||||
|
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
|
||||||
|
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
|
||||||
|
can host it yourself.
|
||||||
|
</Forms.FormText>
|
||||||
|
<Switch
|
||||||
|
key="backend"
|
||||||
|
value={settings.cloud.authenticated}
|
||||||
|
onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }}
|
||||||
|
note="This will request authorization if you have not yet set up cloud integrations."
|
||||||
|
>
|
||||||
|
Enable Cloud Integrations
|
||||||
|
</Switch>
|
||||||
|
<Forms.FormTitle tag="h5">Backend URL</Forms.FormTitle>
|
||||||
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
|
Which backend to use when using cloud integrations.
|
||||||
|
</Forms.FormText>
|
||||||
|
<CheckedTextInput
|
||||||
|
key="backendUrl"
|
||||||
|
value={settings.cloud.url}
|
||||||
|
onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }}
|
||||||
|
validate={validateUrl}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className={Margins.top8}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
disabled={!settings.cloud.authenticated}
|
||||||
|
onClick={() => Alerts.show({
|
||||||
|
title: "Are you sure?",
|
||||||
|
body: "Once your data is erased, we cannot recover it. There's no going back!",
|
||||||
|
onConfirm: eraseAllData,
|
||||||
|
confirmText: "Erase it!",
|
||||||
|
confirmColor: "vc-cloud-erase-data-danger-btn",
|
||||||
|
cancelText: "Nevermind"
|
||||||
|
})}
|
||||||
|
>Erase All Data</Button>
|
||||||
|
<Forms.FormDivider className={Margins.top16} />
|
||||||
|
</Forms.FormSection >
|
||||||
|
<SettingsSyncSection />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(CloudTab);
|
@ -117,7 +117,7 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
||||||
<TextArea
|
<TextArea
|
||||||
value={themeText}
|
value={themeText}
|
||||||
onChange={e => setThemeText(e.currentTarget.value)}
|
onChange={setThemeText}
|
||||||
className={`${TextAreaProps.textarea} vc-settings-theme-links`}
|
className={`${TextAreaProps.textarea} vc-settings-theme-links`}
|
||||||
placeholder="Theme Links"
|
placeholder="Theme Links"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
@ -26,7 +26,7 @@ import { ErrorCard } from "@components/ErrorCard";
|
|||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity, useAwaiter } from "@utils/misc";
|
import { identity, useAwaiter } from "@utils/misc";
|
||||||
import { relaunch } from "@utils/native";
|
import { relaunch, showItemInFolder } from "@utils/native";
|
||||||
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-settings-");
|
const cl = classNameFactory("vc-settings-");
|
||||||
@ -65,7 +65,7 @@ function VencordSettings() {
|
|||||||
title: "Enable React Developer Tools",
|
title: "Enable React Developer Tools",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
},
|
},
|
||||||
!IS_WEB && (!isWindows ? {
|
!IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? {
|
||||||
key: "frameless",
|
key: "frameless",
|
||||||
title: "Disable the window frame",
|
title: "Disable the window frame",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
@ -112,8 +112,7 @@ function VencordSettings() {
|
|||||||
Open QuickCSS File
|
Open QuickCSS File
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
// FIXME: Vencord Desktop support
|
onClick={() => showItemInFolder(settingsDir)}
|
||||||
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={settingsDirPending}>
|
disabled={settingsDirPending}>
|
||||||
Open Settings Folder
|
Open Settings Folder
|
||||||
|
@ -24,6 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
|
|||||||
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
|
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
|
||||||
|
|
||||||
import BackupRestoreTab from "./BackupRestoreTab";
|
import BackupRestoreTab from "./BackupRestoreTab";
|
||||||
|
import CloudTab from "./CloudTab";
|
||||||
import PluginsTab from "./PluginsTab";
|
import PluginsTab from "./PluginsTab";
|
||||||
import ThemesTab from "./ThemesTab";
|
import ThemesTab from "./ThemesTab";
|
||||||
import Updater from "./Updater";
|
import Updater from "./Updater";
|
||||||
@ -45,7 +46,8 @@ const SettingsTabs: Record<string, SettingsTab> = {
|
|||||||
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
|
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
|
||||||
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
|
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
|
||||||
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
|
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
|
||||||
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> },
|
VencordCloud: { name: "Cloud", component: () => <CloudTab /> },
|
||||||
|
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
|
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
|
||||||
|
@ -46,3 +46,14 @@
|
|||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border: 1px solid var(--background-modifier-accent);
|
border: 1px solid var(--background-modifier-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-cloud-settings-sync-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-cloud-erase-data-danger-btn {
|
||||||
|
color: var(--white-500);
|
||||||
|
background-color: var(--button-danger-background);
|
||||||
|
}
|
||||||
|
@ -24,7 +24,7 @@ export async function calculateHashes() {
|
|||||||
const hashes = {} as Record<string, string>;
|
const hashes = {} as Record<string, string>;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
|
[IS_DISCORD_DESKTOP ? "patcher.js" : "main.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
|
||||||
const fis = createReadStream(join(__dirname, file));
|
const fis = createReadStream(join(__dirname, file));
|
||||||
const hash = createHash("sha1", { encoding: "hex" });
|
const hash = createHash("sha1", { encoding: "hex" });
|
||||||
fis.once("end", () => {
|
fis.once("end", () => {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BadgePosition, ProfileBadge } from "@api/Badges";
|
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
@ -53,14 +53,14 @@ const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "tooltip">
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BadgeAPI",
|
name: "BadgeAPI",
|
||||||
description: "API to add badges to users.",
|
description: "API to add badges to users.",
|
||||||
authors: [Devs.Megu],
|
authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
|
||||||
required: true,
|
required: true,
|
||||||
patches: [
|
patches: [
|
||||||
/* Patch the badges array */
|
/* Patch the badges array */
|
||||||
{
|
{
|
||||||
find: "PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP.format({date:",
|
find: "Messages.PROFILE_USER_BADGES,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /&&((\w{1,3})\.push\({tooltip:\w{1,3}\.\w{1,3}\.Messages\.PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP\.format.+?;)(?:return\s\w{1,3};?})/,
|
match: /&&((\i)\.push\({tooltip:\i\.\i\.Messages\.PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP\.format.+?;)(?:return\s\i;?})/,
|
||||||
replace: (_, m, badgeArray) => `&&${m} return Vencord.Api.Badges.inject(${badgeArray}, arguments[0]);}`,
|
replace: (_, m, badgeArray) => `&&${m} return Vencord.Api.Badges.inject(${badgeArray}, arguments[0]);}`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -69,21 +69,23 @@ export default definePlugin({
|
|||||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
|
match: /src:(\i)\[(\i)\.key\],/g,
|
||||||
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
|
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
|
||||||
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
|
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /spacing:(\d{1,2}),children:(.{1,40}(\i)\.jsx.+?(\i)\.onClick.+?\)})},/,
|
match: /children:function(?<=(\i)\.(?:tooltip|description),spacing:\d.+?)/g,
|
||||||
// if the badge provides it's own component, render that instead of an image
|
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) : function"
|
||||||
// the badge also includes info about the user that has it (type BadgeUserArgs), which is why it's passed as props
|
|
||||||
replace: (_, s, origBadgeComponent, React, badge) =>
|
|
||||||
`spacing:${s},children:${badge}.component ? () => (0,${React}.jsx)(${badge}.component, { ...${badge} }) : ${origBadgeComponent}},`
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {
|
||||||
|
const Component = badge.component!;
|
||||||
|
return <Component {...badge} />;
|
||||||
|
}, { noop: true }),
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
Vencord.Api.Badges.addBadge(ContributorBadge);
|
Vencord.Api.Badges.addBadge(ContributorBadge);
|
||||||
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv").then(r => r.text());
|
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv").then(r => r.text());
|
||||||
|
@ -16,77 +16,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { type PatchReplacement } from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { addListener, removeListener } from "@webpack";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last var name corresponding to the Context Menu API (Discord, not ours) module
|
|
||||||
*/
|
|
||||||
let lastVarName = "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param target The patch replacement object
|
|
||||||
* @param exportKey The key exporting the build Context Menu component function
|
|
||||||
*/
|
|
||||||
function makeReplacementProxy(target: PatchReplacement, exportKey: string) {
|
|
||||||
return new Proxy(target, {
|
|
||||||
get(_, p) {
|
|
||||||
if (p === "match") return RegExp(`${exportKey},{(?<=${lastVarName}\\.${exportKey},{)`, "g");
|
|
||||||
// @ts-expect-error
|
|
||||||
return Reflect.get(...arguments);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function listener(exports: any, id: number) {
|
|
||||||
if (!Settings.plugins.ContextMenuAPI.enabled) return removeListener(listener);
|
|
||||||
|
|
||||||
if (typeof exports !== "object" || exports === null) return;
|
|
||||||
|
|
||||||
for (const key in exports) if (key.length <= 3) {
|
|
||||||
const prop = exports[key];
|
|
||||||
if (typeof prop !== "function") continue;
|
|
||||||
|
|
||||||
const str = Function.prototype.toString.call(prop);
|
|
||||||
if (str.includes('path:["empty"]')) {
|
|
||||||
Vencord.Plugins.patches.push({
|
|
||||||
plugin: "ContextMenuAPI",
|
|
||||||
all: true,
|
|
||||||
noWarn: true,
|
|
||||||
find: "navId:",
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
// Set the lastVarName for our proxy to use
|
|
||||||
match: RegExp(`${id}(?<=(\\i)=.+?)`),
|
|
||||||
replace: (id, varName) => {
|
|
||||||
lastVarName = varName;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* We are using a proxy here to utilize the whole code the patcher gives us, instead of matching the entire module (which is super slow)
|
|
||||||
* Our proxy returns the corresponding match for that module utilizing lastVarName, which is set by the patch before
|
|
||||||
*/
|
|
||||||
makeReplacementProxy({
|
|
||||||
match: "", // Needed to canonicalizeDescriptor
|
|
||||||
replace: "$&contextMenuApiArguments:arguments,",
|
|
||||||
}, key)
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
removeListener(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener(listener);
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ContextMenuAPI",
|
name: "ContextMenuAPI",
|
||||||
description: "API for adding/removing items to/from context menus.",
|
description: "API for adding/removing items to/from context menus.",
|
||||||
authors: [Devs.Nuckyz],
|
authors: [Devs.Nuckyz, Devs.Ven],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "♫ (つ。◕‿‿◕。)つ ♪",
|
find: "♫ (つ。◕‿‿◕。)つ ♪",
|
||||||
@ -94,6 +30,14 @@ export default definePlugin({
|
|||||||
match: /(?<=function \i\((\i)\){)(?=var \i,\i=\i\.navId)/,
|
match: /(?<=function \i\((\i)\){)(?=var \i,\i=\i\.navId)/,
|
||||||
replace: (_, props) => `Vencord.Api.ContextMenu._patchContextMenu(${props});`
|
replace: (_, props) => `Vencord.Api.ContextMenu._patchContextMenu(${props});`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".Menu,{",
|
||||||
|
all: true,
|
||||||
|
replacement: {
|
||||||
|
match: /Menu,{(?<=\.jsxs?\)\(\i\.Menu,{)/g,
|
||||||
|
replace: "$&contextMenuApiArguments:typeof arguments!=='undefined'?arguments:[],"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -67,7 +67,7 @@ const settings = definePluginSettings({
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterFolders",
|
name: "BetterFolders",
|
||||||
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
|
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
|
||||||
authors: [Devs.juby],
|
authors: [Devs.juby, Devs.AutumnVN],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '("guildsnav")',
|
find: '("guildsnav")',
|
||||||
@ -122,7 +122,7 @@ export default definePlugin({
|
|||||||
settings,
|
settings,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const getGuildFolder = (id: string) => GuildFolderStore.guildFolders.find(f => f.guildIds.includes(id));
|
const getGuildFolder = (id: string) => GuildFolderStore.getGuildFolders().find(f => f.guildIds.includes(id));
|
||||||
|
|
||||||
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onSwitch = data => {
|
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onSwitch = data => {
|
||||||
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
|
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
|
||||||
|
@ -43,6 +43,7 @@ const settings = definePluginSettings({
|
|||||||
|
|
||||||
let crashCount: number = 0;
|
let crashCount: number = 0;
|
||||||
let lastCrashTimestamp: number = 0;
|
let lastCrashTimestamp: number = 0;
|
||||||
|
let shouldAttemptNextHandle = false;
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "CrashHandler",
|
name: "CrashHandler",
|
||||||
@ -72,6 +73,10 @@ export default definePlugin({
|
|||||||
],
|
],
|
||||||
|
|
||||||
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
||||||
|
if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true;
|
||||||
|
|
||||||
|
shouldAttemptNextHandle = false;
|
||||||
|
|
||||||
if (++crashCount > 5) {
|
if (++crashCount > 5) {
|
||||||
try {
|
try {
|
||||||
showNotification({
|
showNotification({
|
||||||
@ -151,6 +156,7 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
shouldAttemptNextHandle = true;
|
||||||
_this.forceUpdate();
|
_this.forceUpdate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
|
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
|
||||||
|
@ -17,20 +17,28 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
||||||
import { migratePluginSettings, Settings } from "@api/settings";
|
import { definePluginSettings, migratePluginSettings, Settings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
import { ApngBlendOp, ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
||||||
import { getCurrentGuild } from "@utils/discord";
|
import { getCurrentGuild } from "@utils/discord";
|
||||||
import { proxyLazy } from "@utils/proxyLazy";
|
import { proxyLazy } from "@utils/proxyLazy";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||||
import { ChannelStore, FluxDispatcher, PermissionStore, UserStore } from "@webpack/common";
|
import { ChannelStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
|
||||||
|
import type { Message } from "discord-types/general";
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
const DRAFT_TYPE = 0;
|
||||||
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
||||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
||||||
const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings");
|
const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings");
|
||||||
const ReaderFactory = findByPropsLazy("readerFactory");
|
const ReaderFactory = findByPropsLazy("readerFactory");
|
||||||
|
const StickerStore = findStoreLazy("StickersStore") as {
|
||||||
|
getPremiumPacks(): StickerPack[];
|
||||||
|
getAllGuildStickers(): Map<string, Sticker[]>;
|
||||||
|
getStickerById(id: string): Sticker | undefined;
|
||||||
|
};
|
||||||
|
const EmojiStore = findStoreLazy("EmojiStore");
|
||||||
|
|
||||||
|
|
||||||
function searchProtoClass(localName: string, parentProtoClass: any) {
|
function searchProtoClass(localName: string, parentProtoClass: any) {
|
||||||
if (!parentProtoClass) return;
|
if (!parentProtoClass) return;
|
||||||
@ -86,18 +94,69 @@ interface StickerPack {
|
|||||||
stickers: Sticker[];
|
stickers: Sticker[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
|
||||||
|
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
|
||||||
|
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
enableEmojiBypass: {
|
||||||
|
description: "Allow sending fake emojis",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
emojiSize: {
|
||||||
|
description: "Size of the emojis when sending",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
default: 48,
|
||||||
|
markers: [32, 48, 64, 128, 160, 256, 512]
|
||||||
|
},
|
||||||
|
transformEmojis: {
|
||||||
|
description: "Whether to transform fake emojis into real ones",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
enableStickerBypass: {
|
||||||
|
description: "Allow sending fake stickers",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
stickerSize: {
|
||||||
|
description: "Size of the stickers when sending",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
default: 160,
|
||||||
|
markers: [32, 64, 128, 160, 256, 512]
|
||||||
|
},
|
||||||
|
transformStickers: {
|
||||||
|
description: "Whether to transform fake stickers into real ones",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
enableStreamQualityBypass: {
|
||||||
|
description: "Allow streaming in nitro quality",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
migratePluginSettings("FakeNitro", "NitroBypass");
|
migratePluginSettings("FakeNitro", "NitroBypass");
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FakeNitro",
|
name: "FakeNitro",
|
||||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz],
|
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
||||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".PREMIUM_LOCKED;",
|
find: ".PREMIUM_LOCKED;",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
predicate: () => settings.store.enableEmojiBypass,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=(\i)=\i\.intention)/,
|
match: /(?<=(\i)=\i\.intention)/,
|
||||||
@ -115,7 +174,7 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "canUseAnimatedEmojis:function",
|
find: "canUseAnimatedEmojis:function",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
predicate: () => settings.store.enableEmojiBypass,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g,
|
match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g,
|
||||||
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
||||||
@ -123,7 +182,7 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "canUseStickersEverywhere:function",
|
find: "canUseStickersEverywhere:function",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
predicate: () => settings.store.enableStickerBypass,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /canUseStickersEverywhere:function\(\i\){/,
|
match: /canUseStickersEverywhere:function\(\i\){/,
|
||||||
replace: "$&return true;"
|
replace: "$&return true;"
|
||||||
@ -131,7 +190,7 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "\"SENDABLE\"",
|
find: "\"SENDABLE\"",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
predicate: () => settings.store.enableStickerBypass,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(\w+)\.available\?/,
|
match: /(\w+)\.available\?/,
|
||||||
replace: "true?"
|
replace: "true?"
|
||||||
@ -139,7 +198,7 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "canStreamHighQuality:function",
|
find: "canStreamHighQuality:function",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
|
predicate: () => settings.store.enableStreamQualityBypass,
|
||||||
replacement: [
|
replacement: [
|
||||||
"canUseHighVideoUploadQuality",
|
"canUseHighVideoUploadQuality",
|
||||||
"canStreamHighQuality",
|
"canStreamHighQuality",
|
||||||
@ -153,7 +212,7 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "STREAM_FPS_OPTION.format",
|
find: "STREAM_FPS_OPTION.format",
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
|
predicate: () => settings.store.enableStreamQualityBypass,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
|
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
|
||||||
replace: ""
|
replace: ""
|
||||||
@ -186,34 +245,61 @@ export default definePlugin({
|
|||||||
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
|
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
find: 'jumboable?"jumbo":"default"',
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
|
|
||||||
replacement: {
|
|
||||||
match: /jumboable\?"jumbo":"default",emojiId.+?}}\)},(?<=(\i)=function\(\i\){var \i=\i\.node.+?)/,
|
|
||||||
replace: (m, component) => `${m}fakeNitroEmojiComponentExport=($self.EmojiComponent=${component},void 0),`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
|
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
|
||||||
predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
|
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
|
predicate: () => settings.store.transformEmojis,
|
||||||
match: /1!==(\i)\.length\|\|1!==\i\.length/,
|
match: /1!==(\i)\.length\|\|1!==\i\.length/,
|
||||||
replace: (m, content) => `${m}||${content}[0].target?.startsWith("https://cdn.discordapp.com/emojis/")`
|
replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
|
||||||
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
|
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
|
||||||
replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content},arguments[2]?.formatInline);`
|
replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "renderEmbeds=function",
|
find: "renderEmbeds=function",
|
||||||
predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
|
replacement: [
|
||||||
|
{
|
||||||
|
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
|
||||||
|
match: /(renderEmbeds=function\((\i)\){)(.+?embeds\.map\(\(function\((\i)\){)/,
|
||||||
|
replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
predicate: () => settings.store.transformStickers,
|
||||||
|
match: /renderStickersAccessories=function\((\i)\){var (\i)=\(0,\i\.\i\)\(\i\),/,
|
||||||
|
replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message}),`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
predicate: () => settings.store.transformStickers,
|
||||||
|
match: /renderAttachments=function\(\i\){var (\i)=\i.attachments.+?;/,
|
||||||
|
replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".STICKER_IN_MESSAGE_HOVER,",
|
||||||
|
predicate: () => settings.store.transformStickers,
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /var (\i)=\i\.renderableSticker,.{0,50}closePopout.+?channel:\i,closePopout:\i,/,
|
||||||
|
replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /emojiSection.{0,50}description:\i(?<=(\i)\.sticker,.+?)(?=,)/,
|
||||||
|
replace: (m, props) => `${m}+(${props}.renderableSticker?.fake?" This is a Fake Nitro sticker. Only you can see it rendered like a real one, for non Vencord users it will show as a link.":"")`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".Messages.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION",
|
||||||
|
predicate: () => settings.store.transformEmojis,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /renderEmbeds=function\(\i\){.+?embeds\.map\(\(function\((\i)\){/,
|
match: /((\i)=\i\.node,\i=\i\.emojiSourceDiscoverableGuild)(.+?return) (.{0,450}Messages\.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION.+?}\))/,
|
||||||
replace: (m, embed) => `${m}if(${embed}.url?.startsWith("https://cdn.discordapp.com/emojis/"))return null;`
|
replace: (_, rest1, node, rest2, messages) => `${rest1},fakeNitroNode=${node}${rest2}(${messages})+(fakeNitroNode.fake?" This is a Fake Nitro emoji. Only you can see it rendered like a real one, for non Vencord users it will show as a link.":"")`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -331,37 +417,146 @@ export default definePlugin({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
EmojiComponent: null as any,
|
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
|
||||||
|
if (content.length > 1) return content;
|
||||||
patchFakeNitroEmojis(content: Array<any>, inline: boolean) {
|
|
||||||
if (!this.EmojiComponent) return content;
|
|
||||||
|
|
||||||
const newContent: Array<any> = [];
|
const newContent: Array<any> = [];
|
||||||
|
|
||||||
|
let nextIndex = content.length;
|
||||||
|
|
||||||
for (const element of content) {
|
for (const element of content) {
|
||||||
if (element.props?.trusted == null) {
|
if (element.props?.trusted == null) {
|
||||||
newContent.push(element);
|
newContent.push(element);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fakeNitroMatch = element.props.href.match(/https:\/\/cdn\.discordapp\.com\/emojis\/(\d+?)\.(png|webp|gif).+?(?=\s|$)/);
|
if (settings.store.transformEmojis) {
|
||||||
if (!fakeNitroMatch) {
|
const fakeNitroMatch = element.props.href.match(fakeNitroEmojiRegex);
|
||||||
|
if (fakeNitroMatch) {
|
||||||
|
let url: URL | null = null;
|
||||||
|
try {
|
||||||
|
url = new URL(element.props.href);
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
|
||||||
|
|
||||||
|
newContent.push(Parser.defaultRules.customEmoji.react({
|
||||||
|
jumboable: !inline,
|
||||||
|
animated: fakeNitroMatch[2] === "gif",
|
||||||
|
emojiId: fakeNitroMatch[1],
|
||||||
|
name: emojiName,
|
||||||
|
fake: true
|
||||||
|
}, void 0, { key: String(nextIndex++) }));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.store.transformStickers) {
|
||||||
|
if (fakeNitroStickerRegex.test(element.props.href)) continue;
|
||||||
|
|
||||||
|
const gifMatch = element.props.href.match(fakeNitroGifStickerRegex);
|
||||||
|
if (gifMatch) {
|
||||||
|
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
||||||
|
if (StickerStore.getStickerById(gifMatch[1])) continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newContent.push(element);
|
newContent.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstTextElementIdx = newContent.findIndex(element => typeof element === "string");
|
||||||
|
if (firstTextElementIdx !== -1) newContent[firstTextElementIdx] = newContent[firstTextElementIdx].trimStart();
|
||||||
|
|
||||||
|
return newContent;
|
||||||
|
},
|
||||||
|
|
||||||
|
patchFakeNitroStickers(stickers: Array<any>, message: Message) {
|
||||||
|
const itemsToMaybePush: Array<string> = [];
|
||||||
|
|
||||||
|
const contentItems = message.content.split(/\s/);
|
||||||
|
if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]);
|
||||||
|
|
||||||
|
itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url));
|
||||||
|
|
||||||
|
for (const item of itemsToMaybePush) {
|
||||||
|
const imgMatch = item.match(fakeNitroStickerRegex);
|
||||||
|
if (imgMatch) {
|
||||||
|
let url: URL | null = null;
|
||||||
|
try {
|
||||||
|
url = new URL(item);
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
const stickerName = StickerStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroSticker";
|
||||||
|
stickers.push({
|
||||||
|
format_type: 1,
|
||||||
|
id: imgMatch[1],
|
||||||
|
name: stickerName,
|
||||||
|
fake: true
|
||||||
|
});
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
newContent.push((
|
const gifMatch = item.match(fakeNitroGifStickerRegex);
|
||||||
<this.EmojiComponent node={{
|
if (gifMatch) {
|
||||||
type: "customEmoji",
|
if (!StickerStore.getStickerById(gifMatch[1])) continue;
|
||||||
jumboable: !inline && content.length === 1,
|
|
||||||
animated: fakeNitroMatch[2] === "gif",
|
const stickerName = StickerStore.getStickerById(gifMatch[1])?.name ?? "FakeNitroSticker";
|
||||||
name: ":FakeNitroEmoji:",
|
stickers.push({
|
||||||
emojiId: fakeNitroMatch[1]
|
format_type: 2,
|
||||||
}} />
|
id: gifMatch[1],
|
||||||
));
|
name: stickerName,
|
||||||
|
fake: true
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newContent;
|
return stickers;
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
|
||||||
|
if (message.content.split(/\s/).length > 1) return false;
|
||||||
|
|
||||||
|
switch (embed.type) {
|
||||||
|
case "image": {
|
||||||
|
if (settings.store.transformEmojis) {
|
||||||
|
if (fakeNitroEmojiRegex.test(embed.url!)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.store.transformStickers) {
|
||||||
|
if (fakeNitroStickerRegex.test(embed.url!)) return true;
|
||||||
|
|
||||||
|
const gifMatch = embed.url!.match(fakeNitroGifStickerRegex);
|
||||||
|
if (gifMatch) {
|
||||||
|
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
||||||
|
if (StickerStore.getStickerById(gifMatch[1])) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
filterAttachments(attachments: Message["attachments"]) {
|
||||||
|
return attachments.filter(attachment => {
|
||||||
|
if (attachment.content_type !== "image/gif") return true;
|
||||||
|
|
||||||
|
const match = attachment.url.match(fakeNitroGifStickerRegex);
|
||||||
|
if (match) {
|
||||||
|
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
||||||
|
if (StickerStore.getStickerById(match[1])) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldKeepEmojiLink(link: any) {
|
||||||
|
return link.target && fakeNitroEmojiRegex.test(link.target);
|
||||||
},
|
},
|
||||||
|
|
||||||
hasPermissionToUseExternalEmojis(channelId: string) {
|
hasPermissionToUseExternalEmojis(channelId: string) {
|
||||||
@ -407,8 +602,17 @@ export default definePlugin({
|
|||||||
const scale = resolution / Math.max(width, height);
|
const scale = resolution / Math.max(width, height);
|
||||||
ctx.scale(scale, scale);
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
let lastImg: HTMLImageElement | null = null;
|
let previousFrameData: ImageData;
|
||||||
for (const { left, top, width, height, disposeOp, img, delay } of frames) {
|
|
||||||
|
for (const frame of frames) {
|
||||||
|
const { left, top, width, height, img, delay, blendOp, disposeOp } = frame;
|
||||||
|
|
||||||
|
previousFrameData = ctx.getImageData(left, top, width, height);
|
||||||
|
|
||||||
|
if (blendOp === ApngBlendOp.SOURCE) {
|
||||||
|
ctx.clearRect(left, top, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.drawImage(img, left, top, width, height);
|
ctx.drawImage(img, left, top, width, height);
|
||||||
|
|
||||||
const { data } = ctx.getImageData(0, 0, resolution, resolution);
|
const { data } = ctx.getImageData(0, 0, resolution, resolution);
|
||||||
@ -419,19 +623,18 @@ export default definePlugin({
|
|||||||
gif.writeFrame(index, resolution, resolution, {
|
gif.writeFrame(index, resolution, resolution, {
|
||||||
transparent: true,
|
transparent: true,
|
||||||
palette,
|
palette,
|
||||||
delay,
|
delay
|
||||||
});
|
});
|
||||||
|
|
||||||
if (disposeOp === ApngDisposeOp.BACKGROUND) {
|
if (disposeOp === ApngDisposeOp.BACKGROUND) {
|
||||||
ctx.clearRect(left, top, width, height);
|
ctx.clearRect(left, top, width, height);
|
||||||
} else if (disposeOp === ApngDisposeOp.PREVIOUS && lastImg) {
|
} else if (disposeOp === ApngDisposeOp.PREVIOUS) {
|
||||||
ctx.drawImage(lastImg, left, top, width, height);
|
ctx.putImageData(previousFrameData, left, top);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastImg = img;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gif.finish();
|
gif.finish();
|
||||||
|
|
||||||
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
||||||
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
||||||
},
|
},
|
||||||
@ -442,13 +645,6 @@ export default definePlugin({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiStore = findByPropsLazy("getCustomEmojiById");
|
|
||||||
const StickerStore = findByPropsLazy("getAllGuildStickers") as {
|
|
||||||
getPremiumPacks(): StickerPack[];
|
|
||||||
getAllGuildStickers(): Map<string, Sticker[]>;
|
|
||||||
getStickerById(id: string): Sticker | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getWordBoundary(origStr: string, offset: number) {
|
function getWordBoundary(origStr: string, offset: number) {
|
||||||
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
|
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
|
||||||
}
|
}
|
||||||
@ -469,7 +665,7 @@ export default definePlugin({
|
|||||||
|
|
||||||
let link = this.getStickerLink(sticker.id);
|
let link = this.getStickerLink(sticker.id);
|
||||||
if (sticker.format_type === 2) {
|
if (sticker.format_type === 2) {
|
||||||
this.sendAnimatedSticker(this.getStickerLink(sticker.id), sticker.id, channelId);
|
this.sendAnimatedSticker(link, sticker.id, channelId);
|
||||||
return { cancel: true };
|
return { cancel: true };
|
||||||
} else {
|
} else {
|
||||||
if ("pack_id" in sticker) {
|
if ("pack_id" in sticker) {
|
||||||
@ -483,7 +679,7 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete extra.stickerIds;
|
delete extra.stickerIds;
|
||||||
messageObj.content += " " + link;
|
messageObj.content += " " + link + `&name=${encodeURIComponent(sticker.name)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,7 +689,10 @@ export default definePlugin({
|
|||||||
if (emoji.guildId === guildId && !emoji.animated) continue;
|
if (emoji.guildId === guildId && !emoji.animated) continue;
|
||||||
|
|
||||||
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
||||||
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
|
const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({
|
||||||
|
size: Settings.plugins.FakeNitro.emojiSize,
|
||||||
|
name: encodeURIComponent(emoji.name)
|
||||||
|
}));
|
||||||
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
|
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
|
||||||
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
||||||
});
|
});
|
||||||
@ -513,7 +712,10 @@ export default definePlugin({
|
|||||||
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
|
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
|
||||||
if (!emoji.require_colons) continue;
|
if (!emoji.require_colons) continue;
|
||||||
|
|
||||||
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
|
const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({
|
||||||
|
size: Settings.plugins.FakeNitro.emojiSize,
|
||||||
|
name: encodeURIComponent(emoji.name)
|
||||||
|
}));
|
||||||
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
|
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
|
||||||
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
||||||
});
|
});
|
||||||
|
@ -19,12 +19,16 @@
|
|||||||
import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands";
|
import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByProps } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { RestAPI, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
|
const FriendInvites = findByPropsLazy("createFriendInvite");
|
||||||
|
const uuid = findByPropsLazy("v4", "v1");
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FriendInvites",
|
name: "FriendInvites",
|
||||||
description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).",
|
description: "Create and manage friend invite links via slash commands (/create friend invite, /view friend invites, /revoke friend invites).",
|
||||||
authors: [Devs.afn],
|
authors: [Devs.afn, Devs.Dziurwa],
|
||||||
dependencies: ["CommandsAPI"],
|
dependencies: ["CommandsAPI"],
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
@ -32,14 +36,31 @@ export default definePlugin({
|
|||||||
description: "Generates a friend invite link.",
|
description: "Generates a friend invite link.",
|
||||||
inputType: ApplicationCommandInputType.BOT,
|
inputType: ApplicationCommandInputType.BOT,
|
||||||
execute: async (_, ctx) => {
|
execute: async (_, ctx) => {
|
||||||
const friendInvites = findByProps("createFriendInvite");
|
if (!UserStore.getCurrentUser().phone)
|
||||||
const createInvite = await friendInvites.createFriendInvite();
|
return sendBotMessage(ctx.channel.id, {
|
||||||
|
content: "You need to have a phone number connected to your account to create a friend invite!"
|
||||||
|
});
|
||||||
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
const random = uuid.v4();
|
||||||
|
const invite = await RestAPI.post({
|
||||||
|
url: "/friend-finder/find-friends",
|
||||||
|
body: {
|
||||||
|
modified_contacts: {
|
||||||
|
[random]: [1, "", ""]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then(res =>
|
||||||
|
FriendInvites.createFriendInvite({
|
||||||
|
code: res.body.invite_suggestions[0][3],
|
||||||
|
recipient_phone_number_or_email: random
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
sendBotMessage(ctx.channel.id, {
|
||||||
content: `
|
content: `
|
||||||
discord.gg/${createInvite.code} ·
|
discord.gg/${invite.code} ·
|
||||||
Expires: <t:${new Date(createInvite.expires_at).getTime() / 1000}:R> ·
|
Expires: <t:${new Date(invite.expires_at).getTime() / 1000}:R> ·
|
||||||
Max uses: \`${createInvite.max_uses}\`
|
Max uses: \`${invite.max_uses}\`
|
||||||
`.trim().replace(/\s+/g, " ")
|
`.trim().replace(/\s+/g, " ")
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -49,15 +70,16 @@ export default definePlugin({
|
|||||||
description: "View a list of all generated friend invites.",
|
description: "View a list of all generated friend invites.",
|
||||||
inputType: ApplicationCommandInputType.BOT,
|
inputType: ApplicationCommandInputType.BOT,
|
||||||
execute: async (_, ctx) => {
|
execute: async (_, ctx) => {
|
||||||
const friendInvites = findByProps("createFriendInvite");
|
const invites = await FriendInvites.getAllFriendInvites();
|
||||||
const invites = await friendInvites.getAllFriendInvites();
|
|
||||||
const friendInviteList = invites.map(i =>
|
const friendInviteList = invites.map(i =>
|
||||||
`_discord.gg/${i.code}_ ·
|
`
|
||||||
|
_discord.gg/${i.code}_ ·
|
||||||
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> ·
|
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> ·
|
||||||
Times used: \`${i.uses}/${i.max_uses}\``.trim().replace(/\s+/g, " ")
|
Times used: \`${i.uses}/${i.max_uses}\`
|
||||||
|
`.trim().replace(/\s+/g, " ")
|
||||||
);
|
);
|
||||||
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
sendBotMessage(ctx.channel.id, {
|
||||||
content: friendInviteList.join("\n") || "You have no active friend invites!"
|
content: friendInviteList.join("\n") || "You have no active friend invites!"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -67,7 +89,7 @@ export default definePlugin({
|
|||||||
description: "Revokes all generated friend invites.",
|
description: "Revokes all generated friend invites.",
|
||||||
inputType: ApplicationCommandInputType.BOT,
|
inputType: ApplicationCommandInputType.BOT,
|
||||||
execute: async (_, ctx) => {
|
execute: async (_, ctx) => {
|
||||||
await findByProps("createFriendInvite").revokeFriendInvites();
|
await FriendInvites.revokeFriendInvites();
|
||||||
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
return void sendBotMessage(ctx.channel.id, {
|
||||||
content: "All friend invites have been revoked."
|
content: "All friend invites have been revoked."
|
||||||
|
198
src/plugins/imageZoom/components/Magnifier.tsx
Normal file
198
src/plugins/imageZoom/components/Magnifier.tsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
||||||
|
|
||||||
|
import { ELEMENT_ID } from "../constants";
|
||||||
|
import { settings } from "../index";
|
||||||
|
import { waitFor } from "../utils/waitFor";
|
||||||
|
|
||||||
|
interface Vec2 {
|
||||||
|
x: number,
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MagnifierProps {
|
||||||
|
zoom: number;
|
||||||
|
size: number,
|
||||||
|
instance: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => {
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
|
||||||
|
const [imagePosition, setImagePosition] = useState<Vec2>({ x: 0, y: 0 });
|
||||||
|
const [opacity, setOpacity] = useState(0);
|
||||||
|
|
||||||
|
const isShiftDown = useRef(false);
|
||||||
|
|
||||||
|
const zoom = useRef(initalZoom);
|
||||||
|
const size = useRef(initialSize);
|
||||||
|
|
||||||
|
const element = useRef<HTMLDivElement | null>(null);
|
||||||
|
const currentVideoElementRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
const originalVideoElementRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
|
// since we accessing document im gonna use useLayoutEffect
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Shift") {
|
||||||
|
isShiftDown.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Shift") {
|
||||||
|
isShiftDown.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const syncVideos = () => {
|
||||||
|
currentVideoElementRef.current!.currentTime = originalVideoElementRef.current!.currentTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMousePosition = (e: MouseEvent) => {
|
||||||
|
if (instance.state.mouseOver && instance.state.mouseDown) {
|
||||||
|
const offset = size.current / 2;
|
||||||
|
const pos = { x: e.pageX, y: e.pageY };
|
||||||
|
const x = -((pos.x - element.current!.getBoundingClientRect().left) * zoom.current - offset);
|
||||||
|
const y = -((pos.y - element.current!.getBoundingClientRect().top) * zoom.current - offset);
|
||||||
|
setLensPosition({ x: e.x - offset, y: e.y - offset });
|
||||||
|
setImagePosition({ x, y });
|
||||||
|
setOpacity(1);
|
||||||
|
} else {
|
||||||
|
setOpacity(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
if (instance.state.mouseOver && e.button === 0 /* left click */) {
|
||||||
|
zoom.current = settings.store.zoom;
|
||||||
|
size.current = settings.store.size;
|
||||||
|
|
||||||
|
// close context menu if open
|
||||||
|
if (document.getElementById("image-context")) {
|
||||||
|
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMousePosition(e);
|
||||||
|
setOpacity(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
setOpacity(0);
|
||||||
|
if (settings.store.saveZoomValues) {
|
||||||
|
settings.store.zoom = zoom.current;
|
||||||
|
settings.store.size = size.current;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWheel = async (e: WheelEvent) => {
|
||||||
|
if (instance.state.mouseOver && instance.state.mouseDown && !isShiftDown.current) {
|
||||||
|
const val = zoom.current + ((e.deltaY / 100) * (settings.store.invertScroll ? -1 : 1)) * settings.store.zoomSpeed;
|
||||||
|
zoom.current = val <= 1 ? 1 : val;
|
||||||
|
updateMousePosition(e);
|
||||||
|
}
|
||||||
|
if (instance.state.mouseOver && instance.state.mouseDown && isShiftDown.current) {
|
||||||
|
const val = size.current + (e.deltaY * (settings.store.invertScroll ? -1 : 1)) * settings.store.zoomSpeed;
|
||||||
|
size.current = val <= 50 ? 50 : val;
|
||||||
|
updateMousePosition(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
waitFor(() => instance.state.readyState === "READY", () => {
|
||||||
|
const elem = document.getElementById(ELEMENT_ID) as HTMLDivElement;
|
||||||
|
element.current = elem;
|
||||||
|
elem.firstElementChild!.setAttribute("draggable", "false");
|
||||||
|
if (instance.props.animated) {
|
||||||
|
originalVideoElementRef.current = elem!.querySelector("video")!;
|
||||||
|
originalVideoElementRef.current.addEventListener("timeupdate", syncVideos);
|
||||||
|
setReady(true);
|
||||||
|
} else {
|
||||||
|
setReady(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", onKeyDown);
|
||||||
|
document.addEventListener("keyup", onKeyUp);
|
||||||
|
document.addEventListener("mousemove", updateMousePosition);
|
||||||
|
document.addEventListener("mousedown", onMouseDown);
|
||||||
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
|
document.addEventListener("wheel", onWheel);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", onKeyDown);
|
||||||
|
document.removeEventListener("keyup", onKeyUp);
|
||||||
|
document.removeEventListener("mousemove", updateMousePosition);
|
||||||
|
document.removeEventListener("mousedown", onMouseDown);
|
||||||
|
document.removeEventListener("mouseup", onMouseUp);
|
||||||
|
document.removeEventListener("wheel", onWheel);
|
||||||
|
|
||||||
|
if (settings.store.saveZoomValues) {
|
||||||
|
settings.store.zoom = zoom.current;
|
||||||
|
settings.store.size = size.current;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!ready) return null;
|
||||||
|
|
||||||
|
const box = element.current!.getBoundingClientRect();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="lens"
|
||||||
|
style={{
|
||||||
|
opacity,
|
||||||
|
width: size.current + "px",
|
||||||
|
height: size.current + "px",
|
||||||
|
transform: `translate(${lensPosition.x}px, ${lensPosition.y}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{instance.props.animated ?
|
||||||
|
(
|
||||||
|
<video
|
||||||
|
ref={currentVideoElementRef}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${imagePosition.x}px`,
|
||||||
|
top: `${imagePosition.y}px`
|
||||||
|
}}
|
||||||
|
width={`${box.width * zoom.current}px`}
|
||||||
|
height={`${box.height * zoom.current}px`}
|
||||||
|
poster={instance.props.src}
|
||||||
|
src={originalVideoElementRef.current?.src ?? instance.props.src}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
ref={imageRef}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
transform: `translate(${imagePosition.x}px, ${imagePosition.y}px)`
|
||||||
|
}}
|
||||||
|
width={`${box.width * zoom.current}px`}
|
||||||
|
height={`${box.height * zoom.current}px`}
|
||||||
|
src={instance.props.src} alt=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
19
src/plugins/imageZoom/constants.ts
Normal file
19
src/plugins/imageZoom/constants.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ELEMENT_ID = "magnify-modal";
|
234
src/plugins/imageZoom/index.tsx
Normal file
234
src/plugins/imageZoom/index.tsx
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
|
import { definePluginSettings } from "@api/settings";
|
||||||
|
import { makeRange } from "@components/PluginSettings/components";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { debounce } from "@utils/debounce";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { Menu, React, ReactDOM } from "@webpack/common";
|
||||||
|
import type { Root } from "react-dom/client";
|
||||||
|
|
||||||
|
import { Magnifier, MagnifierProps } from "./components/Magnifier";
|
||||||
|
import { ELEMENT_ID } from "./constants";
|
||||||
|
|
||||||
|
export const settings = definePluginSettings({
|
||||||
|
saveZoomValues: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Whether to save zoom and lens size values",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
preventCarouselFromClosingOnClick: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
// Thanks chat gpt
|
||||||
|
description: "Allow the image modal in the image slideshow thing / carousel to remain open when clicking on the image",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
invertScroll: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Invert scroll",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
zoom: {
|
||||||
|
description: "Zoom of the lens",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
markers: makeRange(1, 50, 4),
|
||||||
|
default: 2,
|
||||||
|
stickToMarkers: false,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
description: "Radius / Size of the lens",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
markers: makeRange(50, 1000, 50),
|
||||||
|
default: 100,
|
||||||
|
stickToMarkers: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
zoomSpeed: {
|
||||||
|
description: "How fast the zoom / lens size changes",
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
markers: makeRange(0.1, 5, 0.2),
|
||||||
|
default: 0.5,
|
||||||
|
stickToMarkers: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, _) => {
|
||||||
|
if (!children.some(child => child?.props?.id === "image-zoom")) {
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuGroup id="image-zoom">
|
||||||
|
{/* thanks SpotifyControls */}
|
||||||
|
<Menu.MenuControlItem
|
||||||
|
id="zoom"
|
||||||
|
label="Zoom"
|
||||||
|
control={(props, ref) => (
|
||||||
|
<Menu.MenuSliderControl
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
minValue={1}
|
||||||
|
maxValue={50}
|
||||||
|
value={settings.store.zoom}
|
||||||
|
onChange={debounce((value: number) => settings.store.zoom = value, 100)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Menu.MenuControlItem
|
||||||
|
id="size"
|
||||||
|
label="Lens Size"
|
||||||
|
control={(props, ref) => (
|
||||||
|
<Menu.MenuSliderControl
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
minValue={50}
|
||||||
|
maxValue={1000}
|
||||||
|
value={settings.store.size}
|
||||||
|
onChange={debounce((value: number) => settings.store.size = value, 100)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Menu.MenuControlItem
|
||||||
|
id="zoom-speed"
|
||||||
|
label="Zoom Speed"
|
||||||
|
control={(props, ref) => (
|
||||||
|
<Menu.MenuSliderControl
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
minValue={0.1}
|
||||||
|
maxValue={5}
|
||||||
|
value={settings.store.zoomSpeed}
|
||||||
|
onChange={debounce((value: number) => settings.store.zoomSpeed = value, 100)}
|
||||||
|
renderValue={(value: number) => `${value.toFixed(3)}x`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Menu.MenuGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ImageZoom",
|
||||||
|
description: "Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size",
|
||||||
|
authors: [Devs.Aria],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: '"renderLinkComponent","maxWidth"',
|
||||||
|
replacement: {
|
||||||
|
match: /(return\(.{1,100}\(\)\.wrapper.{1,100})(src)/,
|
||||||
|
replace: `$1id: '${ELEMENT_ID}',$2`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
find: "handleImageLoad=",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /(render=function\(\){.{1,500}limitResponsiveWidth.{1,600})onMouseEnter:/,
|
||||||
|
replace: "$1...$self.makeProps(this),onMouseEnter:"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
match: /componentDidMount=function\(\){/,
|
||||||
|
replace: "$&$self.renderMagnifier(this);",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
match: /componentWillUnmount=function\(\){/,
|
||||||
|
replace: "$&$self.unMountMagnifier();"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
find: ".carouselModal,",
|
||||||
|
replacement: {
|
||||||
|
match: /onClick:(\i),/,
|
||||||
|
replace: "onClick:$self.settings.store.preventCarouselFromClosingOnClick ? () => {} : $1,"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
// to stop from rendering twice /shrug
|
||||||
|
currentMagnifierElement: null as React.FunctionComponentElement<MagnifierProps & JSX.IntrinsicAttributes> | null,
|
||||||
|
element: null as HTMLDivElement | null,
|
||||||
|
|
||||||
|
Magnifier,
|
||||||
|
root: null as Root | null,
|
||||||
|
makeProps(instance) {
|
||||||
|
return {
|
||||||
|
onMouseOver: () => this.onMouseOver(instance),
|
||||||
|
onMouseOut: () => this.onMouseOut(instance),
|
||||||
|
onMouseDown: (e: React.MouseEvent) => this.onMouseDown(e, instance),
|
||||||
|
onMouseUp: () => this.onMouseUp(instance),
|
||||||
|
id: instance.props.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMagnifier(instance) {
|
||||||
|
if (instance.props.id === ELEMENT_ID) {
|
||||||
|
if (!this.currentMagnifierElement) {
|
||||||
|
this.currentMagnifierElement = <Magnifier size={settings.store.size} zoom={settings.store.zoom} instance={instance} />;
|
||||||
|
this.root = ReactDOM.createRoot(this.element!);
|
||||||
|
this.root.render(this.currentMagnifierElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unMountMagnifier() {
|
||||||
|
this.root?.unmount();
|
||||||
|
this.currentMagnifierElement = null;
|
||||||
|
this.root = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseOver(instance) {
|
||||||
|
instance.setState((state: any) => ({ ...state, mouseOver: true }));
|
||||||
|
},
|
||||||
|
onMouseOut(instance) {
|
||||||
|
instance.setState((state: any) => ({ ...state, mouseOver: false }));
|
||||||
|
},
|
||||||
|
onMouseDown(e: React.MouseEvent, instance) {
|
||||||
|
if (e.button === 0 /* left */)
|
||||||
|
instance.setState((state: any) => ({ ...state, mouseDown: true }));
|
||||||
|
},
|
||||||
|
onMouseUp(instance) {
|
||||||
|
instance.setState((state: any) => ({ ...state, mouseDown: false }));
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
addContextMenuPatch("image-context", imageContextMenuPatch);
|
||||||
|
this.element = document.createElement("div");
|
||||||
|
this.element.classList.add("MagnifierContainer");
|
||||||
|
document.body.appendChild(this.element);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
// so componenetWillUnMount gets called if Magnifier component is still alive
|
||||||
|
this.root && this.root.unmount();
|
||||||
|
this.element?.remove();
|
||||||
|
removeContextMenuPatch("image-context", imageContextMenuPatch);
|
||||||
|
}
|
||||||
|
});
|
31
src/plugins/imageZoom/styles.css
Normal file
31
src/plugins/imageZoom/styles.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.lens {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
border: 2px solid grey;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: none;
|
||||||
|
box-shadow: inset 0 0 10px 2px grey;
|
||||||
|
filter: drop-shadow(0 0 2px grey);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom img {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make the carousel take up less space so we can click the backdrop and exit out of it */
|
||||||
|
[class^="focusLock"] > [class^="carouselModal"] {
|
||||||
|
height: fit-content;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class^="focusLock"] > [class^="carouselModal"] > div {
|
||||||
|
height: fit-content;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
22
src/plugins/imageZoom/utils/waitFor.ts
Normal file
22
src/plugins/imageZoom/utils/waitFor.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function waitFor(condition: () => boolean, cb: () => void) {
|
||||||
|
if (condition()) cb();
|
||||||
|
else requestAnimationFrame(() => waitFor(condition, cb));
|
||||||
|
}
|
@ -119,6 +119,7 @@ export default definePlugin({
|
|||||||
name: "InvisibleChat",
|
name: "InvisibleChat",
|
||||||
description: "Encrypt your Messages in a non-suspicious way! This plugin makes requests to >>https://embed.sammcheese.net<< to provide embeds to decrypted links!",
|
description: "Encrypt your Messages in a non-suspicious way! This plugin makes requests to >>https://embed.sammcheese.net<< to provide embeds to decrypted links!",
|
||||||
authors: [Devs.SammCheese],
|
authors: [Devs.SammCheese],
|
||||||
|
dependencies: ["MessagePopoverAPI"],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
// Indicator
|
// Indicator
|
||||||
|
280
src/plugins/moreUserTags.ts
Normal file
280
src/plugins/moreUserTags.ts
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { proxyLazy } from "@utils/proxyLazy.js";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { find, findByPropsLazy } from "@webpack";
|
||||||
|
import { ChannelStore, GuildStore } from "@webpack/common";
|
||||||
|
import { Channel, Message, User } from "discord-types/general";
|
||||||
|
|
||||||
|
type PermissionName = "CREATE_INSTANT_INVITE" | "KICK_MEMBERS" | "BAN_MEMBERS" | "ADMINISTRATOR" | "MANAGE_CHANNELS" | "MANAGE_GUILD" | "CHANGE_NICKNAME" | "MANAGE_NICKNAMES" | "MANAGE_ROLES" | "MANAGE_WEBHOOKS" | "MANAGE_GUILD_EXPRESSIONS" | "CREATE_GUILD_EXPRESSIONS" | "VIEW_AUDIT_LOG" | "VIEW_CHANNEL" | "VIEW_GUILD_ANALYTICS" | "VIEW_CREATOR_MONETIZATION_ANALYTICS" | "MODERATE_MEMBERS" | "SEND_MESSAGES" | "SEND_TTS_MESSAGES" | "MANAGE_MESSAGES" | "EMBED_LINKS" | "ATTACH_FILES" | "READ_MESSAGE_HISTORY" | "MENTION_EVERYONE" | "USE_EXTERNAL_EMOJIS" | "ADD_REACTIONS" | "USE_APPLICATION_COMMANDS" | "MANAGE_THREADS" | "CREATE_PUBLIC_THREADS" | "CREATE_PRIVATE_THREADS" | "USE_EXTERNAL_STICKERS" | "SEND_MESSAGES_IN_THREADS" | "CONNECT" | "SPEAK" | "MUTE_MEMBERS" | "DEAFEN_MEMBERS" | "MOVE_MEMBERS" | "USE_VAD" | "PRIORITY_SPEAKER" | "STREAM" | "USE_EMBEDDED_ACTIVITIES" | "USE_SOUNDBOARD" | "USE_EXTERNAL_SOUNDS" | "REQUEST_TO_SPEAK" | "MANAGE_EVENTS" | "CREATE_EVENTS";
|
||||||
|
|
||||||
|
interface Tag {
|
||||||
|
// name used for identifying, must be alphanumeric + underscores
|
||||||
|
name: string;
|
||||||
|
// name shown on the tag itself, can be anything probably; automatically uppercase'd
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
permissions?: PermissionName[];
|
||||||
|
condition?(message: Message | null, user: User, channel: Channel): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLYDE_ID = "1081004946872352958";
|
||||||
|
|
||||||
|
// PermissionStore.computePermissions is not the same function and doesn't work here
|
||||||
|
const PermissionUtil = findByPropsLazy("computePermissions", "canEveryoneRole") as {
|
||||||
|
computePermissions({ ...args }): bigint;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Permissions = findByPropsLazy("SEND_MESSAGES", "VIEW_CREATOR_MONETIZATION_ANALYTICS") as Record<PermissionName, bigint>;
|
||||||
|
const Tags = proxyLazy(() => find(m => m.Types?.[0] === "BOT").Types) as Record<string, number>;
|
||||||
|
|
||||||
|
const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
|
||||||
|
|
||||||
|
const tags: Tag[] = [
|
||||||
|
{
|
||||||
|
name: "WEBHOOK",
|
||||||
|
displayName: "Webhook",
|
||||||
|
description: "Messages sent by webhooks",
|
||||||
|
condition: isWebhook
|
||||||
|
}, {
|
||||||
|
name: "OWNER",
|
||||||
|
displayName: "Owner",
|
||||||
|
description: "Owns the server",
|
||||||
|
condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id
|
||||||
|
}, {
|
||||||
|
name: "ADMINISTRATOR",
|
||||||
|
displayName: "Admin",
|
||||||
|
description: "Has the administrator permission",
|
||||||
|
permissions: ["ADMINISTRATOR"]
|
||||||
|
}, {
|
||||||
|
name: "MODERATOR_STAFF",
|
||||||
|
displayName: "Staff",
|
||||||
|
description: "Can manage the server, channels or roles",
|
||||||
|
permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"]
|
||||||
|
}, {
|
||||||
|
name: "MODERATOR",
|
||||||
|
displayName: "Mod",
|
||||||
|
description: "Can manage messages or kick/ban people",
|
||||||
|
permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"]
|
||||||
|
}, {
|
||||||
|
name: "VOICE_MODERATOR",
|
||||||
|
displayName: "VC Mod",
|
||||||
|
description: "Can manage voice chats",
|
||||||
|
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
dontShowForBots: {
|
||||||
|
description: "Don't show tags (not including the webhook tag) for bots",
|
||||||
|
type: OptionType.BOOLEAN
|
||||||
|
},
|
||||||
|
dontShowBotTag: {
|
||||||
|
description: "Don't show [BOT] text for bots with other tags (verified bots will still have checkmark)",
|
||||||
|
type: OptionType.BOOLEAN
|
||||||
|
},
|
||||||
|
...Object.fromEntries(tags.map(({ name, displayName, description }) => [
|
||||||
|
`visibility_${name}`, {
|
||||||
|
description: `Show ${displayName} tags (${description})`,
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Always",
|
||||||
|
value: "always",
|
||||||
|
default: true
|
||||||
|
}, {
|
||||||
|
label: "Only in chat",
|
||||||
|
value: "chat"
|
||||||
|
}, {
|
||||||
|
label: "Only in member list and profiles",
|
||||||
|
value: "not-chat"
|
||||||
|
}, {
|
||||||
|
label: "Never",
|
||||||
|
value: "never"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "MoreUserTags",
|
||||||
|
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
|
||||||
|
authors: [Devs.Cyn, Devs.TheSun],
|
||||||
|
settings,
|
||||||
|
patches: [
|
||||||
|
// add tags to the tag list
|
||||||
|
{
|
||||||
|
find: '.BOT=0]="BOT"',
|
||||||
|
replacement: [
|
||||||
|
// add tags to the exported tags list (the Tags variable here)
|
||||||
|
{
|
||||||
|
match: /(\i)\[.\.BOT=0\]="BOT";/,
|
||||||
|
replace: "$&$1=$self.addTagVariants($1);"
|
||||||
|
},
|
||||||
|
// make the tag show the right text
|
||||||
|
{
|
||||||
|
match: /(switch\((\i)\){.+?)case (\i)\.BOT:default:(\i)=(\i\.\i\.Messages)\.BOT_TAG_BOT/,
|
||||||
|
replace: (_, origSwitch, variant, tags, displayedText, strings) =>
|
||||||
|
`${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}`
|
||||||
|
},
|
||||||
|
// show OP tags correctly
|
||||||
|
{
|
||||||
|
match: /(\i)=(\i)===\i\.ORIGINAL_POSTER/,
|
||||||
|
replace: "$1=$self.isOPTag($2)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// in messages
|
||||||
|
{
|
||||||
|
find: ".Types.ORIGINAL_POSTER",
|
||||||
|
replacement: {
|
||||||
|
match: /return null==(\i)\?null:\(0,/,
|
||||||
|
replace: "$1=$self.getTag({...arguments[0],origType:$1,location:'chat'});$&"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// in the member list
|
||||||
|
{
|
||||||
|
find: ".renderBot=function(){",
|
||||||
|
replacement: {
|
||||||
|
match: /this.props.user;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag/,
|
||||||
|
replace: "this.props.user;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});\
|
||||||
|
return type!==null?$2.botTag,type"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// pass channel id down props to be used in profiles
|
||||||
|
{
|
||||||
|
find: ".hasAvatarForGuild(null==",
|
||||||
|
replacement: {
|
||||||
|
match: /\.usernameSection,user/,
|
||||||
|
replace: ".usernameSection,moreTags_channelId:arguments[0].channelId,user"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'copyMetaData:"User Tag"',
|
||||||
|
replacement: {
|
||||||
|
match: /discriminatorClass:(.{1,100}),botClass:/,
|
||||||
|
replace: "discriminatorClass:$1,moreTags_channelId:arguments[0].moreTags_channelId,botClass:"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// in profiles
|
||||||
|
{
|
||||||
|
find: ",botType:",
|
||||||
|
replacement: {
|
||||||
|
match: /,botType:(\i\((\i)\)),/g,
|
||||||
|
replace: ",botType:$self.getTag({user:$2,channelId:arguments[0].moreTags_channelId,origType:$1,location:'not-chat'}),"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
getPermissions(user: User, channel: Channel): string[] {
|
||||||
|
const guild = GuildStore.getGuild(channel?.guild_id);
|
||||||
|
if (!guild) return [];
|
||||||
|
|
||||||
|
const permissions = PermissionUtil.computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
|
||||||
|
return Object.entries(Permissions)
|
||||||
|
.map(([perm, permInt]) =>
|
||||||
|
permissions & permInt ? perm : ""
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
addTagVariants(val: any /* i cant think of a good name */) {
|
||||||
|
let i = 100;
|
||||||
|
tags.forEach(({ name }) => {
|
||||||
|
val[name] = ++i;
|
||||||
|
val[i] = name;
|
||||||
|
val[`${name}-BOT`] = ++i;
|
||||||
|
val[i] = `${name}-BOT`;
|
||||||
|
val[`${name}-OP`] = ++i;
|
||||||
|
val[i] = `${name}-OP`;
|
||||||
|
});
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
|
||||||
|
isOPTag: (tag: number) => tag === Tags.ORIGINAL_POSTER || tags.some(t => tag === Tags[`${t.name}-OP`]),
|
||||||
|
|
||||||
|
getTagText(passedTagName: string, strings: Record<string, string>) {
|
||||||
|
if (!passedTagName) return "BOT";
|
||||||
|
const [tagName, variant] = passedTagName.split("-");
|
||||||
|
const tag = tags.find(({ name }) => tagName === name);
|
||||||
|
if (!tag) return "BOT";
|
||||||
|
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.BOT_TAG_BOT;
|
||||||
|
|
||||||
|
switch (variant) {
|
||||||
|
case "OP":
|
||||||
|
return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tag.displayName}`;
|
||||||
|
case "BOT":
|
||||||
|
return `${strings.BOT_TAG_BOT} • ${tag.displayName}`;
|
||||||
|
default:
|
||||||
|
return tag.displayName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getTag({
|
||||||
|
message, user, channelId, origType, location, channel
|
||||||
|
}: {
|
||||||
|
message?: Message,
|
||||||
|
user: User,
|
||||||
|
channel?: Channel & { isForumPost(): boolean; },
|
||||||
|
channelId?: string;
|
||||||
|
origType?: number;
|
||||||
|
location: string;
|
||||||
|
}): number | null {
|
||||||
|
if (location === "chat" && user.id === "1")
|
||||||
|
return Tags.OFFICIAL;
|
||||||
|
if (user.id === CLYDE_ID)
|
||||||
|
return Tags.AI;
|
||||||
|
|
||||||
|
let type = typeof origType === "number" ? origType : null;
|
||||||
|
|
||||||
|
channel ??= ChannelStore.getChannel(channelId!) as any;
|
||||||
|
if (!channel) return type;
|
||||||
|
|
||||||
|
const settings = this.settings.store;
|
||||||
|
const perms = this.getPermissions(user, channel);
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
switch (settings[`visibility_${tag.name}`]) {
|
||||||
|
case "always":
|
||||||
|
case location:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
tag.permissions?.some(perm => perms.includes(perm)) ||
|
||||||
|
(tag.condition?.(message!, user, channel))
|
||||||
|
) {
|
||||||
|
if (channel.isForumPost() && channel.ownerId === user.id)
|
||||||
|
type = Tags[`${tag.name}-OP`];
|
||||||
|
else if (user.bot && !isWebhook(message!, user) && !settings.dontShowBotTag)
|
||||||
|
type = Tags[`${tag.name}-BOT`];
|
||||||
|
else
|
||||||
|
type = Tags[tag.name];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
});
|
@ -106,6 +106,13 @@ export default definePlugin({
|
|||||||
onClick: makeOnClick("VencordUpdater")
|
onClick: makeOnClick("VencordUpdater")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cats.push({
|
||||||
|
section: "VencordCloud",
|
||||||
|
label: "Cloud",
|
||||||
|
element: () => <SettingsComponent tab="VencordCloud" />,
|
||||||
|
onClick: makeOnClick("VencordCloud")
|
||||||
|
});
|
||||||
|
|
||||||
cats.push({
|
cats.push({
|
||||||
section: "VencordSettingsSync",
|
section: "VencordSettingsSync",
|
||||||
label: "Backup & Restore",
|
label: "Backup & Restore",
|
||||||
|
@ -77,18 +77,14 @@ enum ChannelFlags {
|
|||||||
REQUIRE_TAG = 1 << 4
|
REQUIRE_TAG = 1 << 4
|
||||||
}
|
}
|
||||||
|
|
||||||
let EmojiComponent: ComponentType<any>;
|
|
||||||
let ChannelBeginHeader: ComponentType<any>;
|
let ChannelBeginHeader: ComponentType<any>;
|
||||||
|
|
||||||
export function setEmojiComponent(component: ComponentType<any>) {
|
|
||||||
EmojiComponent = component;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setChannelBeginHeaderComponent(component: ComponentType<any>) {
|
export function setChannelBeginHeaderComponent(component: ComponentType<any>) {
|
||||||
ChannelBeginHeader = component;
|
ChannelBeginHeader = component;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase");
|
const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase");
|
||||||
|
const ChatClasses = findByPropsLazy("chat", "content", "noChat", "chatContent");
|
||||||
const TagComponent = LazyComponent(() => find(m => {
|
const TagComponent = LazyComponent(() => find(m => {
|
||||||
if (typeof m !== "function") return false;
|
if (typeof m !== "function") return false;
|
||||||
|
|
||||||
@ -164,7 +160,7 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ChatScrollClasses.auto + " " + "shc-lock-screen-outer-container"}>
|
<div className={ChatScrollClasses.auto + " " + ChatScrollClasses.customTheme + " " + ChatClasses.chatContent + " " + "shc-lock-screen-outer-container"}>
|
||||||
<div className="shc-lock-screen-container">
|
<div className="shc-lock-screen-container">
|
||||||
<img className="shc-lock-screen-logo" src={HiddenChannelLogo} />
|
<img className="shc-lock-screen-logo" src={HiddenChannelLogo} />
|
||||||
|
|
||||||
@ -245,11 +241,10 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
|
|||||||
{defaultReactionEmoji != null &&
|
{defaultReactionEmoji != null &&
|
||||||
<div className="shc-lock-screen-default-emoji-container">
|
<div className="shc-lock-screen-default-emoji-container">
|
||||||
<Text variant="text-md/normal">Default reaction emoji:</Text>
|
<Text variant="text-md/normal">Default reaction emoji:</Text>
|
||||||
<EmojiComponent node={{
|
{Parser.defaultRules[defaultReactionEmoji.emojiName ? "emoji" : "customEmoji"].react({
|
||||||
type: defaultReactionEmoji.emojiName ? "emoji" : "customEmoji",
|
|
||||||
name: defaultReactionEmoji.emojiName ?? "",
|
name: defaultReactionEmoji.emojiName ?? "",
|
||||||
emojiId: defaultReactionEmoji.emojiId
|
emojiId: defaultReactionEmoji.emojiId
|
||||||
}} />
|
})}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{channel.hasFlag(ChannelFlags.REQUIRE_TAG) &&
|
{channel.hasFlag(ChannelFlags.REQUIRE_TAG) &&
|
||||||
|
@ -27,7 +27,7 @@ import { findByPropsLazy } from "@webpack";
|
|||||||
import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common";
|
import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common";
|
||||||
import { Channel } from "discord-types/general";
|
import { Channel } from "discord-types/general";
|
||||||
|
|
||||||
import HiddenChannelLockScreen, { setChannelBeginHeaderComponent, setEmojiComponent } from "./components/HiddenChannelLockScreen";
|
import HiddenChannelLockScreen, { setChannelBeginHeaderComponent } from "./components/HiddenChannelLockScreen";
|
||||||
|
|
||||||
const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer");
|
const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer");
|
||||||
|
|
||||||
@ -234,14 +234,6 @@ export default definePlugin({
|
|||||||
replace: ".filter(ch=>!$self.isHiddenChannel(ch))"
|
replace: ".filter(ch=>!$self.isHiddenChannel(ch))"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Export the emoji component used on the lock screen
|
|
||||||
{
|
|
||||||
find: 'jumboable?"jumbo":"default"',
|
|
||||||
replacement: {
|
|
||||||
match: /jumboable\?"jumbo":"default",emojiId.+?}}\)},(?<=(\i)=function\(\i\){var \i=\i\.node.+?)/,
|
|
||||||
replace: (m, component) => `${m}shcEmojiComponentExport=($self.setEmojiComponent(${component}),void 0),`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE",
|
find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE",
|
||||||
replacement: [
|
replacement: [
|
||||||
@ -403,7 +395,6 @@ export default definePlugin({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
setEmojiComponent,
|
|
||||||
setChannelBeginHeaderComponent,
|
setChannelBeginHeaderComponent,
|
||||||
|
|
||||||
isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {
|
isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
.shc-lock-screen-outer-container {
|
.shc-lock-screen-outer-container {
|
||||||
background-color: var(--background-primary);
|
|
||||||
overflow: hidden scroll;
|
overflow: hidden scroll;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -41,14 +40,14 @@
|
|||||||
|
|
||||||
.shc-lock-screen-topic-container {
|
.shc-lock-screen-topic-container {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
background-color: var(--background-secondary);
|
background: var(--bg-overlay-3, var(--background-secondary));
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
max-width: 70vw;
|
max-width: 70vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shc-lock-screen-tags-container {
|
.shc-lock-screen-tags-container {
|
||||||
background-color: var(--background-secondary);
|
background: var(--bg-overlay-3, var(--background-secondary));
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
max-width: 70vw;
|
max-width: 70vw;
|
||||||
@ -84,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shc-lock-screen-default-emoji-container > [class^="emojiContainer"] {
|
.shc-lock-screen-default-emoji-container > [class^="emojiContainer"] {
|
||||||
background-color: var(--background-secondary);
|
background: var(--bg-overlay-3, var(--background-secondary));
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 3px 4px;
|
padding: 3px 4px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
@ -94,7 +93,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--background-secondary);
|
background: var(--bg-overlay-3, var(--background-secondary));
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
max-width: 70vw;
|
max-width: 70vw;
|
||||||
|
@ -140,6 +140,7 @@
|
|||||||
|
|
||||||
#vc-spotify-album-image {
|
#vc-spotify-album-image {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
transition: filter 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#vc-spotify-album-image:hover {
|
#vc-spotify-album-image:hover {
|
||||||
|
@ -35,6 +35,7 @@ export default definePlugin({
|
|||||||
required: true,
|
required: true,
|
||||||
description: "Helps us provide support to you",
|
description: "Helps us provide support to you",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
dependencies: ["CommandsAPI"],
|
||||||
|
|
||||||
commands: [{
|
commands: [{
|
||||||
name: "vencord-debug",
|
name: "vencord-debug",
|
||||||
|
18
src/plugins/userVoiceShow/components/VoiceChannelSection.css
Normal file
18
src/plugins/userVoiceShow/components/VoiceChannelSection.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.vc-uvs-button > div {
|
||||||
|
white-space: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-uvs-button {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-uvs-header {
|
||||||
|
color: var(--header-primary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-uvs-modal-margin > [class^="section"] {
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
61
src/plugins/userVoiceShow/components/VoiceChannelSection.tsx
Normal file
61
src/plugins/userVoiceShow/components/VoiceChannelSection.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./VoiceChannelSection.css";
|
||||||
|
|
||||||
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
|
import { Button, Forms, PermissionStore, Toasts } from "@webpack/common";
|
||||||
|
import { Channel } from "discord-types/general";
|
||||||
|
|
||||||
|
const ChannelActions = findByPropsLazy("selectChannel", "selectVoiceChannel");
|
||||||
|
const UserPopoutSection = findByCodeLazy(".lastSection", ".children");
|
||||||
|
|
||||||
|
const CONNECT = 1n << 20n;
|
||||||
|
|
||||||
|
interface VoiceChannelFieldProps {
|
||||||
|
channel: Channel;
|
||||||
|
label: string;
|
||||||
|
showHeader: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VoiceChannelSection = ({ channel, label, showHeader }: VoiceChannelFieldProps) => (
|
||||||
|
<UserPopoutSection>
|
||||||
|
{showHeader && <Forms.FormTitle className="vc-uvs-header">In a voice channel</Forms.FormTitle>}
|
||||||
|
<Button
|
||||||
|
className="vc-uvs-button"
|
||||||
|
color={Button.Colors.TRANSPARENT}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
|
||||||
|
onClick={() => {
|
||||||
|
if (PermissionStore.can(CONNECT, channel))
|
||||||
|
ChannelActions.selectVoiceChannel(channel.id);
|
||||||
|
else
|
||||||
|
Toasts.show({
|
||||||
|
message: "Insufficient permissions to enter the channel.",
|
||||||
|
id: "user-voice-show-insufficient-permissions",
|
||||||
|
type: Toasts.Type.FAILURE,
|
||||||
|
options: {
|
||||||
|
position: Toasts.Position.BOTTOM,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
</UserPopoutSection>
|
||||||
|
);
|
105
src/plugins/userVoiceShow/index.tsx
Normal file
105
src/plugins/userVoiceShow/index.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findStoreLazy } from "@webpack";
|
||||||
|
import { ChannelStore, GuildStore } from "@webpack/common";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
|
import { VoiceChannelSection } from "./components/VoiceChannelSection";
|
||||||
|
|
||||||
|
const VoiceStateStore = findStoreLazy("VoiceStateStore");
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
showInUserProfileModal: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show a user's voice channel in their profile modal",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showVoiceChannelSectionHeader: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: 'Whether to show "IN A VOICE CHANNEL" above the join button',
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UserProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VoiceChannelField = ErrorBoundary.wrap(({ user }: UserProps) => {
|
||||||
|
const { channelId } = VoiceStateStore.getVoiceStateForUser(user.id) ?? {};
|
||||||
|
if (!channelId) return null;
|
||||||
|
|
||||||
|
const channel = ChannelStore.getChannel(channelId);
|
||||||
|
const guild = GuildStore.getGuild(channel.guild_id);
|
||||||
|
|
||||||
|
if (!guild) return null; // When in DM call
|
||||||
|
|
||||||
|
const result = `${guild.name} | ${channel.name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VoiceChannelSection
|
||||||
|
channel={channel}
|
||||||
|
label={result}
|
||||||
|
showHeader={settings.store.showVoiceChannelSectionHeader}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "UserVoiceShow",
|
||||||
|
description: "Shows whether a User is currently in a voice channel somewhere in their profile",
|
||||||
|
authors: [Devs.LordElias],
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patchModal({ user }: UserProps) {
|
||||||
|
if (!settings.store.showInUserProfileModal)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="vc-uvs-modal-margin">
|
||||||
|
<VoiceChannelField user={user} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
patchPopout: ({ user }: UserProps) => <VoiceChannelField user={user} />,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".showCopiableUsername",
|
||||||
|
replacement: {
|
||||||
|
match: /\(0,\w\.jsx\)\(\w{2},{user:\w,setNote/,
|
||||||
|
// paste my fancy custom button above the message field
|
||||||
|
replace: "$self.patchPopout(arguments[0]),$&",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".USER_PROFILE_MODAL",
|
||||||
|
replacement: {
|
||||||
|
match: /,{user:\w{1,2}}\)(?!;case)/,
|
||||||
|
// paste my fancy custom button below the username
|
||||||
|
replace: "$&,$self.patchModal(arguments[0])",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
@ -56,7 +56,7 @@ export default definePlugin({
|
|||||||
find: "AudioContextSettingsMigrated",
|
find: "AudioContextSettingsMigrated",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=updateAsync\("audioContextSettings".{0,350}return \i\.volume=)\i(?=})/,
|
match: /(?<=isLocalMute\(\i,\i\),volume:.+?volume:)\i(?=})/,
|
||||||
replace: "$&>200?200:$&"
|
replace: "$&>200?200:$&"
|
||||||
},
|
},
|
||||||
{
|
{
|
@ -16,24 +16,71 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { saveFile } from "@utils/web";
|
||||||
|
import { findByProps, findLazy } from "@webpack";
|
||||||
|
import { Clipboard } from "@webpack/common";
|
||||||
|
|
||||||
|
async function fetchImage(url: string) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (res.status !== 200) return;
|
||||||
|
|
||||||
|
return await res.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MiniDispatcher = findLazy(m => m.emitter?._events?.INSERT_TEXT);
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
// This needs to be all in one setting because to enable any of these, we need to make Discord use their desktop context
|
||||||
|
// menu handler instead of the web one, which breaks the other menus that aren't enabled
|
||||||
|
addBack: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Add back the Discord context menus for images, links and the chat input bar",
|
||||||
|
// Web slate menu has proper spellcheck suggestions and image context menu is also pretty good,
|
||||||
|
// so disable this by default. Vencord Desktop just doesn't, so enable by default
|
||||||
|
default: IS_VENCORD_DESKTOP,
|
||||||
|
restartNeeded: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "WebContextMenus",
|
name: "WebContextMenus",
|
||||||
description: "Re-adds some of context menu items missing on the web version of Discord, namely Copy/Open Link",
|
description: "Re-adds context menus missing in the web version of Discord: Images, ChatInputBar, Links, 'Copy Link', 'Open Link', 'Copy Image', 'Save Image'",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
|
|
||||||
patches: [{
|
settings,
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (settings.store.addBack) {
|
||||||
|
const ctxMenuCallbacks = findByProps("contextMenuCallbackNative");
|
||||||
|
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
|
||||||
|
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
|
||||||
|
this.changedListeners = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.changedListeners) {
|
||||||
|
const ctxMenuCallbacks = findByProps("contextMenuCallbackNative");
|
||||||
|
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
|
||||||
|
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
// Add back Copy & Open Link
|
||||||
|
{
|
||||||
// There is literally no reason for Discord to make this Desktop only.
|
// There is literally no reason for Discord to make this Desktop only.
|
||||||
// The only thing broken is copy, but they already have a different copy function
|
// The only thing broken is copy, but they already have a different copy function
|
||||||
// with web support????
|
// with web support????
|
||||||
find: "open-native-link",
|
find: "open-native-link",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// if (isNative || null ==
|
// if (IS_DESKTOP || null == ...)
|
||||||
match: /if\(!\w\..{1,3}\|\|null==/,
|
match: /if\(!\i\.\i\|\|null==/,
|
||||||
replace: "if(null=="
|
replace: "if(null=="
|
||||||
},
|
},
|
||||||
// Fix silly Discord calling the non web support copy
|
// Fix silly Discord calling the non web support copy
|
||||||
@ -42,5 +89,138 @@ export default definePlugin({
|
|||||||
replace: "Vencord.Webpack.Common.Clipboard.copy"
|
replace: "Vencord.Webpack.Common.Clipboard.copy"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}]
|
},
|
||||||
|
|
||||||
|
// Add back Copy & Save Image
|
||||||
|
{
|
||||||
|
find: 'id:"copy-image"',
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
// if (!IS_WEB || null ==
|
||||||
|
match: /if\(!\i\.\i\|\|null==/,
|
||||||
|
replace: "if(null=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /return\s*?\[\i\.default\.canCopyImage\(\)/,
|
||||||
|
replace: "return [true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=COPY_IMAGE_MENU_ITEM,)action:/,
|
||||||
|
replace: "action:()=>$self.copyImage(arguments[0]),oldAction:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=SAVE_IMAGE_MENU_ITEM,)action:/,
|
||||||
|
replace: "action:()=>$self.saveImage(arguments[0]),oldAction:"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add back image context menu
|
||||||
|
{
|
||||||
|
find: 'navId:"image-context"',
|
||||||
|
predicate: () => settings.store.addBack,
|
||||||
|
replacement: {
|
||||||
|
// return IS_DESKTOP ? React.createElement(Menu, ...)
|
||||||
|
match: /return \i\.\i\?(?=\(0,\i\.jsxs?\)\(\i\.Menu)/,
|
||||||
|
replace: "return true?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add back link context menu
|
||||||
|
{
|
||||||
|
find: '"interactionUsernameProfile"',
|
||||||
|
predicate: () => settings.store.addBack,
|
||||||
|
replacement: {
|
||||||
|
match: /if\("A"===\i\.tagName&&""!==\i\.textContent\)/,
|
||||||
|
replace: "if(false)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add back slate / text input context menu
|
||||||
|
{
|
||||||
|
find: '"slate-toolbar"',
|
||||||
|
predicate: () => settings.store.addBack,
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=\.handleContextMenu=.+?"bottom";)\i\.\i\?/,
|
||||||
|
replace: "true?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'navId:"textarea-context"',
|
||||||
|
predicate: () => settings.store.addBack,
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
// desktopOnlyEntries = makeEntries(), spellcheckChildren = desktopOnlyEntries[0], languageChildren = desktopOnlyEntries[1]
|
||||||
|
match: /\i=.{0,30}text:\i,target:\i,onHeightUpdate:\i\}\),2\),(\i)=\i\[0\],(\i)=\i\[1\]/,
|
||||||
|
// set spellcheckChildren & languageChildren to empty arrays, so just in case patch 3 fails, we don't
|
||||||
|
// reference undefined variables
|
||||||
|
replace: "$1=[],$2=[]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// if (!IS_DESKTOP) return
|
||||||
|
match: /(?<=showApplicationCommandSuggestions;)if\(!\i\.\i\)/,
|
||||||
|
replace: "if(false)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// do not add menu items for entries removed in patch 1. Using a lookbehind for group 1 is slow,
|
||||||
|
// so just capture and add back
|
||||||
|
match: /("submit-button".+?)(\(0,\i\.jsx\)\(\i\.MenuGroup,\{children:\i\}\),){2}/,
|
||||||
|
replace: "$1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Change calls to DiscordNative.clipboard to us instead
|
||||||
|
match: /\b\i\.default\.(copy|cut|paste)/g,
|
||||||
|
replace: "$self.$1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Maybe add spellcheck for VencordDesktop
|
||||||
|
],
|
||||||
|
|
||||||
|
async copyImage(url: string) {
|
||||||
|
const data = await fetchImage(url);
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
[data.type]: data
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveImage(url: string) {
|
||||||
|
const data = await fetchImage(url);
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const name = url.split("/").pop()!;
|
||||||
|
const file = new File([data], name, { type: data.type });
|
||||||
|
|
||||||
|
saveFile(file);
|
||||||
|
},
|
||||||
|
|
||||||
|
copy() {
|
||||||
|
const selection = document.getSelection();
|
||||||
|
if (!selection) return;
|
||||||
|
|
||||||
|
Clipboard.copy(selection.toString());
|
||||||
|
},
|
||||||
|
|
||||||
|
cut() {
|
||||||
|
this.copy();
|
||||||
|
MiniDispatcher.dispatch("INSERT_TEXT", { rawText: "" });
|
||||||
|
},
|
||||||
|
|
||||||
|
async paste() {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
|
||||||
|
const data = new DataTransfer();
|
||||||
|
data.setData("text/plain", text);
|
||||||
|
|
||||||
|
document.dispatchEvent(
|
||||||
|
new ClipboardEvent("paste", {
|
||||||
|
clipboardData: data
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "Webhook Tags",
|
|
||||||
description: "Changes the bot tag to say webhook for webhooks",
|
|
||||||
authors: [Devs.Cyn],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: '.BOT=0]="BOT"',
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /(.)\[.\.BOT=0\]="BOT";/,
|
|
||||||
replace: (orig, types) =>
|
|
||||||
`${types}[${types}.WEBHOOK=99]="WEBHOOK";${orig}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /case (.)\.BOT:default:(.)=/,
|
|
||||||
replace: (orig, types, text) =>
|
|
||||||
`case ${types}.WEBHOOK:${text}="WEBHOOK";break;${orig}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".Types.ORIGINAL_POSTER",
|
|
||||||
replacement: {
|
|
||||||
match: /return null==(.)\?null:\(0,.{1,3}\.jsxs?\)\((.{1,3}\.\i)/,
|
|
||||||
replace: (orig, type, BotTag) =>
|
|
||||||
`if(arguments[0].message.webhookId&&arguments[0].user.isNonUserBot()){${type}=${BotTag}.Types.WEBHOOK}${orig}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
124
src/utils/cloud.tsx
Normal file
124
src/utils/cloud.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as DataStore from "@api/DataStore";
|
||||||
|
import { showNotification } from "@api/Notifications";
|
||||||
|
import { Settings } from "@api/settings";
|
||||||
|
import { findByProps } from "@webpack";
|
||||||
|
import { UserStore } from "@webpack/common";
|
||||||
|
|
||||||
|
import Logger from "./Logger";
|
||||||
|
import { openModal } from "./modal";
|
||||||
|
|
||||||
|
export const cloudLogger = new Logger("Cloud", "#39b7e0");
|
||||||
|
export const getCloudUrl = () => new URL(Settings.cloud.url);
|
||||||
|
|
||||||
|
export async function getAuthorization() {
|
||||||
|
const secrets = await DataStore.get<Record<string, string>>("Vencord_cloudSecret") ?? {};
|
||||||
|
return secrets[getCloudUrl().origin];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setAuthorization(secret: string) {
|
||||||
|
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
||||||
|
secrets ??= {};
|
||||||
|
secrets[getCloudUrl().origin] = secret;
|
||||||
|
return secrets;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deauthorizeCloud() {
|
||||||
|
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
||||||
|
secrets ??= {};
|
||||||
|
delete secrets[getCloudUrl().origin];
|
||||||
|
return secrets;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authorizeCloud() {
|
||||||
|
if (await getAuthorization() !== undefined) {
|
||||||
|
Settings.cloud.authenticated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl()));
|
||||||
|
var { clientId, redirectUri } = await oauthConfiguration.json();
|
||||||
|
} catch {
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Integration",
|
||||||
|
body: "Setup failed (couldn't retrieve OAuth configuration)."
|
||||||
|
});
|
||||||
|
Settings.cloud.authenticated = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
|
||||||
|
|
||||||
|
openModal((props: any) => <OAuth2AuthorizeModal
|
||||||
|
{...props}
|
||||||
|
scopes={["identify"]}
|
||||||
|
responseType="code"
|
||||||
|
redirectUri={redirectUri}
|
||||||
|
permissions={0n}
|
||||||
|
clientId={clientId}
|
||||||
|
cancelCompletesFlow={false}
|
||||||
|
callback={async (callbackUrl: string) => {
|
||||||
|
if (!callbackUrl) {
|
||||||
|
Settings.cloud.authenticated = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(callbackUrl, {
|
||||||
|
headers: new Headers({ Accept: "application/json" })
|
||||||
|
});
|
||||||
|
const { secret } = await res.json();
|
||||||
|
if (secret) {
|
||||||
|
cloudLogger.info("Authorized with secret");
|
||||||
|
await setAuthorization(secret);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Integration",
|
||||||
|
body: "Cloud integrations enabled!"
|
||||||
|
});
|
||||||
|
Settings.cloud.authenticated = true;
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Integration",
|
||||||
|
body: "Setup failed (no secret returned?)."
|
||||||
|
});
|
||||||
|
Settings.cloud.authenticated = false;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
cloudLogger.error("Failed to authorize", e);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Integration",
|
||||||
|
body: `Setup failed (${e.toString()}).`
|
||||||
|
});
|
||||||
|
Settings.cloud.authenticated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCloudAuth() {
|
||||||
|
const userId = UserStore.getCurrentUser().id;
|
||||||
|
const secret = await getAuthorization();
|
||||||
|
|
||||||
|
return window.btoa(`${secret}:${userId}`);
|
||||||
|
}
|
@ -226,6 +226,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||||||
name: "TheKodeToad",
|
name: "TheKodeToad",
|
||||||
id: 706152404072267788n
|
id: 706152404072267788n
|
||||||
},
|
},
|
||||||
|
LordElias: {
|
||||||
|
name: "LordElias",
|
||||||
|
id: 319460781567639554n
|
||||||
|
},
|
||||||
juby: {
|
juby: {
|
||||||
name: "Juby210",
|
name: "Juby210",
|
||||||
id: 324622488644616195n
|
id: 324622488644616195n
|
||||||
@ -241,5 +245,13 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||||||
skyevg: {
|
skyevg: {
|
||||||
name: "skyevg",
|
name: "skyevg",
|
||||||
id: 1090310844283363348n
|
id: 1090310844283363348n
|
||||||
|
},
|
||||||
|
Dziurwa: {
|
||||||
|
name: "Dziurwa",
|
||||||
|
id: 787017887877169173n
|
||||||
|
},
|
||||||
|
AutumnVN: {
|
||||||
|
name: "AutumnVN",
|
||||||
|
id: 393694671383166998n
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
19
src/utils/localStorage.ts
Normal file
19
src/utils/localStorage.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const { localStorage } = window;
|
@ -22,3 +22,10 @@ export function relaunch() {
|
|||||||
else
|
else
|
||||||
window.VencordDesktop.app.relaunch();
|
window.VencordDesktop.app.relaunch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showItemInFolder(path: string) {
|
||||||
|
if (IS_DISCORD_DESKTOP)
|
||||||
|
window.DiscordNative.fileManager.showItemInFolder(path);
|
||||||
|
else
|
||||||
|
window.VencordDesktop.fileManager.showItemInFolder(path);
|
||||||
|
}
|
||||||
|
@ -16,10 +16,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { showNotification } from "@api/Notifications";
|
||||||
|
import { PlainSettings, Settings } from "@api/settings";
|
||||||
import { Toasts } from "@webpack/common";
|
import { Toasts } from "@webpack/common";
|
||||||
|
import { deflateSync, inflateSync } from "fflate";
|
||||||
|
|
||||||
|
import { getCloudAuth, getCloudUrl } from "./cloud";
|
||||||
import IpcEvents from "./IpcEvents";
|
import IpcEvents from "./IpcEvents";
|
||||||
import Logger from "./Logger";
|
import Logger from "./Logger";
|
||||||
|
import { saveFile } from "./web";
|
||||||
|
|
||||||
export async function importSettings(data: string) {
|
export async function importSettings(data: string) {
|
||||||
try {
|
try {
|
||||||
@ -50,31 +55,22 @@ export async function downloadSettingsBackup() {
|
|||||||
if (IS_DISCORD_DESKTOP) {
|
if (IS_DISCORD_DESKTOP) {
|
||||||
DiscordNative.fileManager.saveWithDialog(data, filename);
|
DiscordNative.fileManager.saveWithDialog(data, filename);
|
||||||
} else {
|
} else {
|
||||||
const file = new File([data], filename, { type: "application/json" });
|
saveFile(new File([data], filename, { type: "application/json" }));
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = URL.createObjectURL(file);
|
|
||||||
a.download = filename;
|
|
||||||
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
setImmediate(() => {
|
|
||||||
URL.revokeObjectURL(a.href);
|
|
||||||
document.body.removeChild(a);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastSuccess = () => Toasts.show({
|
const toast = (type: number, message: string) =>
|
||||||
type: Toasts.Type.SUCCESS,
|
Toasts.show({
|
||||||
message: "Settings successfully imported. Restart to apply changes!",
|
type,
|
||||||
|
message,
|
||||||
id: Toasts.genId()
|
id: Toasts.genId()
|
||||||
});
|
});
|
||||||
|
|
||||||
const toastFailure = (err: any) => Toasts.show({
|
const toastSuccess = () =>
|
||||||
type: Toasts.Type.FAILURE,
|
toast(Toasts.Type.SUCCESS, "Settings successfully imported. Restart to apply changes!");
|
||||||
message: `Failed to import settings: ${String(err)}`,
|
|
||||||
id: Toasts.genId()
|
const toastFailure = (err: any) =>
|
||||||
});
|
toast(Toasts.Type.FAILURE, `Failed to import settings: ${String(err)}`);
|
||||||
|
|
||||||
export async function uploadSettingsBackup(showToast = true): Promise<void> {
|
export async function uploadSettingsBackup(showToast = true): Promise<void> {
|
||||||
if (IS_DISCORD_DESKTOP) {
|
if (IS_DISCORD_DESKTOP) {
|
||||||
@ -121,3 +117,169 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
|
|||||||
setImmediate(() => document.body.removeChild(input));
|
setImmediate(() => document.body.removeChild(input));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cloud settings
|
||||||
|
const cloudSettingsLogger = new Logger("Cloud:Settings", "#39b7e0");
|
||||||
|
|
||||||
|
export async function putCloudSettings() {
|
||||||
|
const settings = await exportSettings();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
|
method: "PUT",
|
||||||
|
headers: new Headers({
|
||||||
|
Authorization: await getCloudAuth(),
|
||||||
|
"Content-Type": "application/octet-stream"
|
||||||
|
}),
|
||||||
|
body: deflateSync(new TextEncoder().encode(settings))
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
cloudSettingsLogger.error(`Failed to sync up, API returned ${res.status}`);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not synchronize settings to cloud (API returned ${res.status}).`,
|
||||||
|
color: "var(--red-360)"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { written } = await res.json();
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = written;
|
||||||
|
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(PlainSettings, null, 4));
|
||||||
|
|
||||||
|
cloudSettingsLogger.info("Settings uploaded to cloud successfully");
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Synchronized your settings to the cloud!",
|
||||||
|
color: "var(--green-360)"
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
cloudSettingsLogger.error("Failed to sync up", e);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not synchronize settings to the cloud (${e.toString()}).`,
|
||||||
|
color: "var(--red-360)"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCloudSettings(shouldNotify = true, force = false) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
|
method: "GET",
|
||||||
|
headers: new Headers({
|
||||||
|
Authorization: await getCloudAuth(),
|
||||||
|
Accept: "application/octet-stream",
|
||||||
|
"If-None-Match": Settings.cloud.settingsSyncVersion.toString()
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 404) {
|
||||||
|
cloudSettingsLogger.info("No settings on the cloud");
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "There are no settings in the cloud."
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 304) {
|
||||||
|
cloudSettingsLogger.info("Settings up to date");
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Your settings are up to date."
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
cloudSettingsLogger.error(`Failed to sync down, API returned ${res.status}`);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not synchronize settings from the cloud (API returned ${res.status}).`,
|
||||||
|
color: "var(--red-360)"
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const written = Number(res.headers.get("etag")!);
|
||||||
|
const localWritten = Settings.cloud.settingsSyncVersion;
|
||||||
|
|
||||||
|
// don't need to check for written > localWritten because the server will return 304 due to if-none-match
|
||||||
|
if (!force && written < localWritten) {
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Your local settings are newer than the cloud ones."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.arrayBuffer();
|
||||||
|
|
||||||
|
const settings = new TextDecoder().decode(inflateSync(new Uint8Array(data)));
|
||||||
|
await importSettings(settings);
|
||||||
|
|
||||||
|
// sync with server timestamp instead of local one
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = written;
|
||||||
|
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(PlainSettings, null, 4));
|
||||||
|
|
||||||
|
cloudSettingsLogger.info("Settings loaded from cloud successfully");
|
||||||
|
if (shouldNotify)
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
||||||
|
color: "var(--green-360)",
|
||||||
|
onClick: () => window.DiscordNative.app.relaunch()
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
cloudSettingsLogger.error("Failed to sync down", e);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not synchronize settings from the cloud (${e.toString()}).`,
|
||||||
|
color: "var(--red-360)"
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCloudSettings() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: new Headers({
|
||||||
|
Authorization: await getCloudAuth()
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
cloudSettingsLogger.error(`Failed to delete, API returned ${res.status}`);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not delete settings (API returned ${res.status}).`,
|
||||||
|
color: "var(--red-360)"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudSettingsLogger.info("Settings deleted from cloud successfully");
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: "Settings deleted from cloud!",
|
||||||
|
color: "var(--green-360)"
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
cloudSettingsLogger.error("Failed to delete", e);
|
||||||
|
showNotification({
|
||||||
|
title: "Cloud Settings",
|
||||||
|
body: `Could not delete settings (${e.toString()}).`,
|
||||||
|
color: "var(--red-360)"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -62,7 +62,7 @@ export function getRepo() {
|
|||||||
return Unwrap(VencordNative.ipc.invoke<IpcRes<string>>(IpcEvents.GET_REPO));
|
return Unwrap(VencordNative.ipc.invoke<IpcRes<string>>(IpcEvents.GET_REPO));
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hashes = Record<"patcher.js" | "preload.js" | "renderer.js" | "renderer.css", string>;
|
type Hashes = Record<"patcher.js" | "main.js" | "preload.js" | "renderer.js" | "renderer.css", string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if hard restart is required
|
* @returns true if hard restart is required
|
||||||
@ -75,8 +75,11 @@ export async function rebuild() {
|
|||||||
|
|
||||||
const newHashes = await Unwrap(VencordNative.ipc.invoke<IpcRes<Hashes>>(IpcEvents.GET_HASHES));
|
const newHashes = await Unwrap(VencordNative.ipc.invoke<IpcRes<Hashes>>(IpcEvents.GET_HASHES));
|
||||||
|
|
||||||
return oldHashes["patcher.js"] !== newHashes["patcher.js"] ||
|
if (oldHashes["preload.js"] !== newHashes["preload.js"]) return true;
|
||||||
oldHashes["preload.js"] !== newHashes["preload.js"];
|
if (IS_DISCORD_DESKTOP && oldHashes["patcher.js"] !== newHashes["patcher.js"]) return true;
|
||||||
|
if (IS_VENCORD_DESKTOP && oldHashes["main.js"] !== newHashes["main.js"]) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {
|
export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {
|
||||||
|
30
src/utils/web.ts
Normal file
30
src/utils/web.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function saveFile(file: File) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(file);
|
||||||
|
a.download = file.name;
|
||||||
|
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
setImmediate(() => {
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
});
|
||||||
|
}
|
4
src/webpack/common/types/components.d.ts
vendored
4
src/webpack/common/types/components.d.ts
vendored
@ -186,7 +186,9 @@ export type TextInput = ComponentType<PropsWithChildren<{
|
|||||||
Sizes: Record<"DEFAULT" | "MINI", string>;
|
Sizes: Record<"DEFAULT" | "MINI", string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TextArea = ComponentType<PropsWithRef<HTMLProps<HTMLTextAreaElement>>>;
|
export type TextArea = ComponentType<PropsWithRef<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & {
|
||||||
|
onChange(v: string): void;
|
||||||
|
}>>;
|
||||||
|
|
||||||
interface SelectOption {
|
interface SelectOption {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -103,8 +103,10 @@ waitFor(["dispatch", "subscribe"], m => {
|
|||||||
|
|
||||||
|
|
||||||
// This is the same module but this is easier
|
// This is the same module but this is easier
|
||||||
waitFor(filters.byCode("currentToast?"), m => Toasts.show = m);
|
waitFor("showToast", m => {
|
||||||
waitFor(filters.byCode("currentToast:null"), m => Toasts.pop = m);
|
Toasts.show = m.showToast;
|
||||||
|
Toasts.pop = m.popToast;
|
||||||
|
});
|
||||||
|
|
||||||
waitFor(["show", "close"], m => Alerts = m);
|
waitFor(["show", "close"], m => Alerts = m);
|
||||||
waitFor("parseTopic", m => Parser = m);
|
waitFor("parseTopic", m => Parser = m);
|
||||||
|
Reference in New Issue
Block a user