/* * 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"; import { DataStore } from ".."; 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"); 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; } 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)); } if (!isSafeObject(parsed)) throw new Error("Unsafe Settings"); if ("settings" in parsed && "quickCss" in parsed) { Object.assign(PlainSettings, parsed.settings); await VencordNative.settings.set(parsed.settings); await VencordNative.quickCss.set(parsed.quickCss); if (parsed.dataStore) await DataStore.setMany(parsed.dataStore); } else throw new Error("Invalid Settings. Is this even a Vencord Settings file?"); } export async function exportSettings({ syncDataStore = true, minify }: { syncDataStore?: boolean; minify?: boolean; }) { const settings = VencordNative.settings.get(); const quickCss = await VencordNative.quickCss.get(); const dataStore = syncDataStore ? await DataStore.entries() : undefined; return JSON.stringify( { settings, quickCss, ...(syncDataStore && { dataStore }) }, null, minify ? undefined : 4 ); } 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); } 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`; 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 { 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); } }