Allow 100+ plugin users to export list & fix DataStore export errors (#767)

* Add idle auto restart plugin

* Add idle auto restart plugin

* Add idle auto restart plugin

* Add idle auto restart plugin update

* Add idle auto restart plugin update

* Add idle auto restart plugin update

* Replace SelectedChannelStore with VoiceStateStore

* updates

* feat: Allow 100+ plugin users to export list as file & fix DataStore export errors

- Support Helper: Allow users with 100+ plugins to send plugin list as .txt file attachment instead of blocking them
- Backup & Restore: Fix 'Failed to read large IndexedDB value' error by only loading DataStore when needed
- Export Plugins/CSS now skip DataStore entirely, preventing crashes on large datastores

* Delete src/equicordplugins/idleAutoRestart directory

* fix lint

* Update supportHelper.tsx

* Update offline.ts

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
StealTech 2026-02-14 14:02:11 -06:00 • committed by GitHub
parent 2cfd13d640
commit 5eefbb1d85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 124 additions and 24 deletions

View file

@ -108,11 +108,27 @@ export async function importSettings(data: string, type: BackupType = "all", clo
export async function exportSettings({ syncDataStore = true, type = "all", minify }: { syncDataStore?: boolean; type?: BackupType; minify?: boolean; }) {
const settings = VencordNative.settings.get();
const quickCss = await VencordNative.quickCss.get();
const dataStore = syncDataStore ? await DataStore.entries() : undefined;
let dataStore: any;
if (syncDataStore) {
try {
dataStore = await DataStore.entries();
} catch (err) {
logger.error("Failed to read DataStore entries:", err);
if (type === "all") {
logger.warn("Skipping DataStore in backup due to size. Export DataStore separately if needed.");
toast(Toasts.Type.MESSAGE, "DataStore too large - exported without it. Use 'Export DataStore' separately if needed.");
dataStore = undefined;
} else if (type === "datastore") {
throw new Error("DataStore is too large to export. Please clear some plugin data and try again.");
}
}
}
switch (type) {
case "all": {
return JSON.stringify({ settings, quickCss, ...(syncDataStore && { dataStore }) }, null, minify ? undefined : 4);
return JSON.stringify({ settings, quickCss, ...(dataStore && { dataStore }) }, null, minify ? undefined : 4);
}
case "plugins": {
return JSON.stringify({ settings }, null, minify ? undefined : 4);
@ -127,14 +143,21 @@ export async function exportSettings({ syncDataStore = true, type = "all", minif
}
export async function downloadSettingsBackup(type: BackupType = "all", { minify }: { minify?: boolean; } = {}) {
const backup = await exportSettings({ minify, type });
const filename = `equicord-${type}-backup-${moment().format("YYYY-MM-DD")}.json`;
const data = new TextEncoder().encode(backup);
try {
const syncDataStore = type === "all" || type === "datastore";
const backup = await exportSettings({ minify, type, syncDataStore });
const filename = `equicord-${type}-backup-${moment().format("YYYY-MM-DD")}.json`;
const data = new TextEncoder().encode(backup);
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(data, filename);
} else {
saveFile(new File([data], filename, { type: "application/json" }));
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(data, filename);
} else {
saveFile(new File([data], filename, { type: "application/json" }));
}
} catch (err) {
logger.error("Failed to export settings:", err);
toast(Toasts.Type.FAILURE, "Failed to export settings, check console");
throw err;
}
}

View file

@ -40,7 +40,8 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Alerts, Button, ChannelStore, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, SelectedChannelStore, showToast, Toasts, UserStore } from "@webpack/common";
import { CloudUploadPlatform } from "@vencord/discord-types/enums";
import { Alerts, Button, ChannelStore, CloudUploader, Constants, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, UserStore } from "@webpack/common";
import { JSX } from "react";
import plugins, { PluginMeta } from "~plugins";
@ -185,6 +186,36 @@ async function generateDebugInfoMessage() {
return content.trim();
}
async function uploadPluginListFile(channelId: string, fileContent: string, filename: string) {
const file = new File([fileContent], filename, { type: "text/plain" });
const upload = new CloudUploader({ file, platform: CloudUploadPlatform.WEB }, channelId);
return new Promise<void>((resolve, reject) => {
upload.on("complete", () => {
RestAPI.post({
url: Constants.Endpoints.MESSAGES(channelId),
body: {
flags: 0,
channel_id: channelId,
content: `⚠️ Plugin list attached as file due to high plugin count (${fileContent.split("\n").filter(l => l.startsWith(" -")).length} plugins enabled)`,
nonce: SnowflakeUtils.fromTimestamp(Date.now()),
sticker_ids: [],
type: 0,
attachments: [{
id: "0",
filename: upload.filename,
uploaded_filename: upload.uploadedFilename,
}],
}
}).then(() => resolve()).catch(reject);
});
upload.on("error", () => reject(new Error("Failed to upload file")));
upload.upload();
});
}
function generatePluginList() {
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
@ -194,29 +225,54 @@ function generatePluginList() {
const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin).sort();
const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin).sort();
let content = `**Enabled Plugins (${enabledStockPlugins.length}):**\n${makeCodeblock(enabledStockPlugins.join(", "))}`;
if (enabledUserPlugins.length) {
content += `**Enabled UserPlugins (${enabledUserPlugins.length}):**\n${makeCodeblock(enabledUserPlugins.join(", "))}`;
}
const user = UserStore.getCurrentUser();
if (enabledPlugins.length > 100 && !isAnyPluginDev(user.id)) {
Alerts.show({
title: "You are attempting to get support!",
title: "Warning: High Plugin Count",
body: <div>
<div style={{ display: "flex", justifyContent: "center", marginBottom: "1rem" }}>
<img src="https://media.tenor.com/QtGqjwBpRzwAAAAi/wumpus-dancing.gif" />
</div>
<Paragraph>Before you ask for help,</Paragraph>
<Paragraph>We do not handle support for users who use 100+ plugins</Paragraph>
<Paragraph>issue could be plugin confliction</Paragraph>
<Paragraph>try removing some plugins and see if it fixes!</Paragraph>
<Paragraph>You have more than 100 plugins enabled.</Paragraph>
<Paragraph>Due to the sheer amount of plugins, you may not receive support.</Paragraph>
<Paragraph>Your issue is likely caused by plugin conflicts.</Paragraph>
<Paragraph>Please consider disabling some plugins to troubleshoot.</Paragraph>
<Paragraph className={Margins.top8}>Your plugin list will be sent as a text file.</Paragraph>
</div>
});
return `${user.username} has more than 100 plugins enabled, please reduce the number of enabled plugins to get support.`;
const fileContent = [
`Enabled Stock Plugins (${enabledStockPlugins.length}):`,
...enabledStockPlugins.map(p => ` - ${p}`),
"",
];
if (enabledUserPlugins.length) {
fileContent.push(
`Enabled User Plugins (${enabledUserPlugins.length}):`,
...enabledUserPlugins.map(p => ` - ${p}`),
""
);
}
fileContent.push(
"---",
`Total Enabled Plugins: ${enabledPlugins.length}`,
"Warning: Due to the high number of enabled plugins, support may be limited."
);
return {
uploadFile: true,
fileContent: fileContent.join("\n"),
filename: `${user.username}-plugins.txt`
};
}
let content = `**Enabled Plugins (${enabledStockPlugins.length}):**\n${makeCodeblock(enabledStockPlugins.join(", "))}`;
if (enabledUserPlugins.length) {
content += `**Enabled UserPlugins (${enabledUserPlugins.length}):**\n${makeCodeblock(enabledUserPlugins.join(", "))}`;
}
return content;
@ -258,9 +314,22 @@ export default definePlugin({
description: "Send Equicord plugin list",
// @ts-ignore
predicate: ctx => isAnyPluginDev(UserStore.getCurrentUser()?.id) || isEquicordGuild(ctx?.guild?.id, true),
execute: () => {
execute: async () => {
const channelId = SelectedChannelStore.getChannelId();
const pluginList = generatePluginList();
return { content: typeof pluginList === "string" ? pluginList : "Unable to generate plugin list." };
if (typeof pluginList === "string") {
return { content: pluginList };
} else if (pluginList && typeof pluginList === "object" && pluginList.uploadFile) {
try {
await uploadPluginListFile(channelId, pluginList.fileContent, pluginList.filename);
return { content: "" }; // Empty return since file was already sent
} catch (e) {
new Logger("SupportHelper").error("Failed to upload plugin list:", e);
return { content: "Failed to upload plugin list file. Please try again." };
}
}
return { content: "Unable to generate plugin list." };
}
}
],
@ -380,6 +449,14 @@ export default definePlugin({
const pluginList = generatePluginList();
if (typeof pluginList === "string") {
sendMessage(props.channel.id, { content: pluginList });
} else if (pluginList && typeof pluginList === "object" && pluginList.uploadFile) {
try {
await uploadPluginListFile(props.channel.id, pluginList.fileContent, pluginList.filename);
showToast("Plugin list uploaded successfully!", Toasts.Type.SUCCESS);
} catch (e) {
new Logger("SupportHelper").error("Failed to upload plugin list:", e);
showToast("Failed to upload plugin list", Toasts.Type.FAILURE);
}
}
}}
>