diff --git a/src/def.d.ts b/src/def.d.ts
index 90e9e98..19a8083 100644
--- a/src/def.d.ts
+++ b/src/def.d.ts
@@ -369,8 +369,13 @@ interface VendettaObject {
};
constants: {
DISCORD_SERVER: string;
+ DISCORD_SERVER_ID: string;
+ PLUGINS_CHANNEL_ID: string;
+ THEMES_CHANNEL_ID: string;
GITHUB: string;
+ PROXY_PREFIX: string;
HTTP_REGEX: RegExp;
+ HTTP_REGEX_MULTI: RegExp;
};
utils: {
findInReactTree: (tree: SearchTree, filter: SearchFilter) => any;
diff --git a/src/index.ts b/src/index.ts
index 1ef2bc8..c1e0e5c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,6 +2,7 @@ import { patchLogHook } from "@lib/debug";
import { patchCommands } from "@lib/commands";
import { initPlugins } from "@lib/plugins";
import { patchAssets } from "@ui/assets";
+import initQuickInstall from "@ui/quickInstall";
import initSettings from "@ui/settings";
import initFixes from "@lib/fixes";
import logger from "@lib/logger";
@@ -15,6 +16,7 @@ export default async () => {
patchCommands(),
initFixes(),
initSettings(),
+ initQuickInstall(),
]);
// Assign window object
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 2fca598..4f1b51d 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,3 +1,8 @@
export const DISCORD_SERVER = "https://discord.gg/n9QQ4XhhJP";
+export const DISCORD_SERVER_ID = "1015931589865246730";
+export const PLUGINS_CHANNEL_ID = "1091880384561684561";
+export const THEMES_CHANNEL_ID = "1091880434939482202";
export const GITHUB = "https://github.com/vendetta-mod";
-export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/;
\ No newline at end of file
+export const PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
+export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/;
+export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
\ No newline at end of file
diff --git a/src/ui/quickInstall/forumPost.tsx b/src/ui/quickInstall/forumPost.tsx
new file mode 100644
index 0000000..96789f2
--- /dev/null
+++ b/src/ui/quickInstall/forumPost.tsx
@@ -0,0 +1,65 @@
+import { findByName, findByProps } from "@metro/filters";
+import { DISCORD_SERVER_ID, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, HTTP_REGEX_MULTI, PROXY_PREFIX } from "@lib/constants";
+import { after } from "@lib/patcher";
+import { installPlugin } from "@lib/plugins";
+import { installTheme } from "@lib/themes";
+import { findInReactTree } from "@lib/utils";
+import { getAssetIDByName } from "@ui/assets";
+import { Forms } from "@ui/components";
+import { showToast } from "@ui/toasts";
+
+const ForumPostLongPressActionSheet = findByName("ForumPostLongPressActionSheet", false);
+const { FormRow } = Forms;
+// Discord uses this Icon in action sheets. FormRow.Icon is too dark.
+const Icon = findByName("Icon");
+
+const { useFirstForumPostMessage } = findByProps("useFirstForumPostMessage");
+const { hideActionSheet } = findByProps("openLazy", "hideActionSheet");
+
+export default () => after("default", ForumPostLongPressActionSheet, ([{ thread }], res) => {
+ if (thread.guild_id !== DISCORD_SERVER_ID) return;
+
+ // Determine what type of addon this is.
+ let postType: string;
+ if (thread.parent_id === PLUGINS_CHANNEL_ID) {
+ postType = "Plugin";
+ } else if (thread.parent_id === THEMES_CHANNEL_ID) {
+ postType = "Theme";
+ } else return;
+
+ const { firstMessage } = useFirstForumPostMessage(thread);
+
+ let urls = firstMessage?.content?.match(HTTP_REGEX_MULTI);
+ if (!urls) return;
+
+ if (postType === "Plugin") {
+ urls = urls.filter((url: string) => url.startsWith(PROXY_PREFIX));
+ } else {
+ urls = urls.filter((url: string) => url.endsWith(".json"));
+ };
+
+ const url = urls[0];
+ if (!url) return;
+
+ /* Assuming that the actions array is at index 1
+ could break in the future, but I doubt Discord
+ will add more to the post action sheet and
+ index 0 will either be quick add reactions or false.
+ */
+ const actions = findInReactTree(res, (t) => t.props?.bottom === true).props.children.props.children[1];
+ const ActionsSection = actions[0].type;
+
+ actions.unshift(
+ }
+ label={`Install ${postType}`}
+ onPress={() =>
+ (postType === "Plugin" ? installPlugin : installTheme)(url).then(() => {
+ showToast(`Successfully installed ${thread.name}`, getAssetIDByName("Check"));
+ }).catch((e: Error) => {
+ showToast(e.message, getAssetIDByName("Small"));
+ }).finally(() => hideActionSheet())
+ }
+ />
+ );
+});
\ No newline at end of file
diff --git a/src/ui/quickInstall/index.ts b/src/ui/quickInstall/index.ts
new file mode 100644
index 0000000..f3dd1bd
--- /dev/null
+++ b/src/ui/quickInstall/index.ts
@@ -0,0 +1,11 @@
+import patchForumPost from "@ui/quickInstall/forumPost";
+import patchUrl from "@ui/quickInstall/url";
+
+export default function initQuickInstall() {
+ const patches = new Array;
+
+ patches.push(patchForumPost());
+ patches.push(patchUrl());
+
+ return () => patches.forEach(p => p());
+};
\ No newline at end of file
diff --git a/src/ui/quickInstall/url.ts b/src/ui/quickInstall/url.ts
new file mode 100644
index 0000000..cb379c5
--- /dev/null
+++ b/src/ui/quickInstall/url.ts
@@ -0,0 +1,29 @@
+import { findByProps } from "@metro/filters";
+import { PROXY_PREFIX } from "@lib/constants";
+import { after } from "@lib/patcher";
+import { installPlugin } from "@lib/plugins";
+import { installTheme } from "@lib/themes";
+import { getAssetIDByName } from "@ui/assets";
+import { showToast } from "@ui/toasts";
+
+const showSimpleActionSheet = findByProps("showSimpleActionSheet");
+
+export default () => after("showSimpleActionSheet", showSimpleActionSheet, ([{ key, header: { title: url }, options }]) => {
+ if (key !== "LongPressUrl") return;
+
+ let urlType: string;
+ if (url.startsWith(PROXY_PREFIX)) {
+ urlType = "Plugin";
+ } else if (url.endsWith(".json")) {
+ urlType = "Theme";
+ } else return;
+
+ options.push({
+ label: `Install ${urlType}`, onPress: () =>
+ (urlType === "Plugin" ? installPlugin : installTheme)(url).then(() => {
+ showToast("Successfully installed", getAssetIDByName("Check"));
+ }).catch((e: Error) => {
+ showToast(e.message, getAssetIDByName("Small"));
+ }),
+ });
+});
\ No newline at end of file