2025-11-16 15:17:35 +01:00
|
|
|
/*
|
|
|
|
|
* Vencord, a Discord client mod
|
|
|
|
|
* Copyright (c) 2025 Vendicated and contributors
|
|
|
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { PlainSettings } from "@api/Settings";
|
|
|
|
|
import { Logger } from "@utils/Logger";
|
|
|
|
|
import { chooseFile, saveFile } from "@utils/web";
|
|
|
|
|
import { moment, Toasts } from "@webpack/common";
|
|
|
|
|
|
2025-11-18 23:48:50 -05:00
|
|
|
import { DataStore } from "..";
|
|
|
|
|
|
2025-11-16 15:17:35 +01:00
|
|
|
const toast = (type: string, message: string) =>
|
|
|
|
|
Toasts.show({
|
|
|
|
|
type,
|
|
|
|
|
message,
|
|
|
|
|
id: Toasts.genId()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const toastSuccess = () =>
|
|
|
|
|
toast(Toasts.Type.SUCCESS, "Settings successfully imported. Restart to apply changes!");
|
|
|
|
|
|
|
|
|
|
const toastFailure = (err: any) =>
|
|
|
|
|
toast(Toasts.Type.FAILURE, `Failed to import settings: ${String(err)}`);
|
|
|
|
|
|
|
|
|
|
const logger = new Logger("SettingsSync:Offline", "#39b7e0");
|
|
|
|
|
|
2025-12-06 21:33:58 +01:00
|
|
|
function isSafeObject(obj: any) {
|
|
|
|
|
if (obj == null || typeof obj !== "object") return true;
|
|
|
|
|
|
|
|
|
|
for (const key in obj) {
|
|
|
|
|
if (["__proto__", "constructor", "prototype"].includes(key)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!isSafeObject(obj[key])) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 15:17:35 +01:00
|
|
|
export async function importSettings(data: string) {
|
|
|
|
|
try {
|
|
|
|
|
var parsed = JSON.parse(data);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(data);
|
|
|
|
|
throw new Error("Failed to parse JSON: " + String(err));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 21:33:58 +01:00
|
|
|
if (!isSafeObject(parsed))
|
|
|
|
|
throw new Error("Unsafe Settings");
|
|
|
|
|
|
2025-11-16 15:17:35 +01:00
|
|
|
if ("settings" in parsed && "quickCss" in parsed) {
|
|
|
|
|
Object.assign(PlainSettings, parsed.settings);
|
|
|
|
|
await VencordNative.settings.set(parsed.settings);
|
|
|
|
|
await VencordNative.quickCss.set(parsed.quickCss);
|
2025-12-04 12:07:19 -05:00
|
|
|
if (parsed.dataStore) await DataStore.setMany(parsed.dataStore);
|
2025-11-16 15:17:35 +01:00
|
|
|
} else
|
|
|
|
|
throw new Error("Invalid Settings. Is this even a Vencord Settings file?");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-04 12:01:17 -05:00
|
|
|
export async function exportSettings({ syncDataStore = true, minify }: { syncDataStore?: boolean; minify?: boolean; }) {
|
2025-11-16 15:17:35 +01:00
|
|
|
const settings = VencordNative.settings.get();
|
|
|
|
|
const quickCss = await VencordNative.quickCss.get();
|
2025-12-04 12:01:17 -05:00
|
|
|
const dataStore = syncDataStore ? await DataStore.entries() : undefined;
|
2025-12-04 11:59:27 -05:00
|
|
|
|
|
|
|
|
return JSON.stringify(
|
|
|
|
|
{
|
|
|
|
|
settings,
|
|
|
|
|
quickCss,
|
2025-12-04 12:01:17 -05:00
|
|
|
...(syncDataStore && { dataStore })
|
2025-12-04 11:59:27 -05:00
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
minify ? undefined : 4
|
|
|
|
|
);
|
2025-11-18 23:48:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function exportPlugins({ minify }: { minify?: boolean; } = {}) {
|
|
|
|
|
const { plugins } = VencordNative.settings.get();
|
|
|
|
|
return JSON.stringify({ settings: { plugins } }, null, minify ? undefined : 4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function exportCSS({ minify }: { minify?: boolean; } = {}) {
|
|
|
|
|
const quickCss = await VencordNative.quickCss.get();
|
|
|
|
|
return JSON.stringify({ quickCss }, null, minify ? undefined : 4);
|
2025-11-16 15:17:35 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-18 23:48:50 -05:00
|
|
|
export async function exportDataStores({ minify }: { minify?: boolean; } = {}) {
|
|
|
|
|
const dataStore = await DataStore.entries();
|
|
|
|
|
return JSON.stringify({ dataStore }, null, minify ? undefined : 4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type BackupType = "settings" | "plugins" | "css" | "datastore";
|
|
|
|
|
|
|
|
|
|
export async function downloadSettingsBackup(type: BackupType, { minify }: { minify?: boolean; } = {}) {
|
|
|
|
|
let backup: string;
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case "settings":
|
|
|
|
|
backup = await exportSettings({ minify });
|
|
|
|
|
break;
|
|
|
|
|
case "plugins":
|
|
|
|
|
backup = await exportPlugins({ minify });
|
|
|
|
|
break;
|
|
|
|
|
case "css":
|
|
|
|
|
backup = await exportCSS({ minify });
|
|
|
|
|
break;
|
|
|
|
|
case "datastore":
|
|
|
|
|
backup = await exportDataStores({ minify });
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Error("Invalid backup type");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filename = `equicord-${type}-backup-${moment().format("YYYY-MM-DD")}.json`;
|
2025-11-16 15:17:35 +01:00
|
|
|
const data = new TextEncoder().encode(backup);
|
|
|
|
|
|
|
|
|
|
if (IS_DISCORD_DESKTOP) {
|
|
|
|
|
DiscordNative.fileManager.saveWithDialog(data, filename);
|
|
|
|
|
} else {
|
|
|
|
|
saveFile(new File([data], filename, { type: "application/json" }));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function uploadSettingsBackup(showToast = true): Promise<void> {
|
|
|
|
|
if (IS_DISCORD_DESKTOP) {
|
|
|
|
|
const [file] = await DiscordNative.fileManager.openFiles({
|
|
|
|
|
filters: [
|
|
|
|
|
{ name: "Vencord Settings Backup", extensions: ["json"] },
|
|
|
|
|
{ name: "all", extensions: ["*"] }
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (file) {
|
|
|
|
|
try {
|
|
|
|
|
await importSettings(new TextDecoder().decode(file.data));
|
|
|
|
|
if (showToast) toastSuccess();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.error(err);
|
|
|
|
|
if (showToast) toastFailure(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const file = await chooseFile("application/json");
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await importSettings(reader.result as string);
|
|
|
|
|
if (showToast) toastSuccess();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.error(err);
|
|
|
|
|
if (showToast) toastFailure(err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
}
|
|
|
|
|
}
|