/* * 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 { t } from "@utils/translation"; import { chooseFile, saveFile } from "@utils/web"; import { moment, Toasts } from "@webpack/common"; import { DataStore } from ".."; type BackupType = "all" | "plugins" | "css" | "datastore"; const toast = (type: string, message: string) => Toasts.show({ type, message, id: Toasts.genId() }); const toastSuccess = () => toast(Toasts.Type.SUCCESS, t("vencord.settingsSync.imported")); const toastFailure = (err: any) => toast(Toasts.Type.FAILURE, t("vencord.settingsSync.failedToImport", { e: 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, type: BackupType = "all", cloud = false) { 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"); switch (type) { case "all": { if (!cloud && (!("settings" in parsed))) throw new Error("Invalid Settings. Plugin settings is required for this import try a different one."); if (parsed.settings) { Object.assign(PlainSettings, parsed.settings); await VencordNative.settings.set(parsed.settings); } if (parsed.quickCss) await VencordNative.quickCss.set(parsed.quickCss); if (parsed.dataStore) await DataStore.setMany(parsed.dataStore); break; } case "plugins": { if (!parsed.settings) throw new Error("Plugin settings missing"); Object.assign(PlainSettings, parsed.settings); await VencordNative.settings.set(parsed.settings); break; } case "css": { if (!parsed.quickCss) throw new Error("CSS missing"); await VencordNative.quickCss.set(parsed.quickCss); break; } case "datastore": { if (!parsed.dataStore) throw new Error("DataStore data missing"); await DataStore.setMany(parsed.dataStore); break; } } } 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; switch (type) { case "all": { return JSON.stringify({ settings, quickCss, ...(syncDataStore && { dataStore }) }, null, minify ? undefined : 4); } case "plugins": { return JSON.stringify({ settings }, null, minify ? undefined : 4); } case "css": { return JSON.stringify({ quickCss }, null, minify ? undefined : 4); } case "datastore": { return JSON.stringify({ dataStore }, null, minify ? undefined : 4); } } } 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); if (IS_DISCORD_DESKTOP) { DiscordNative.fileManager.saveWithDialog(data, filename); } else { saveFile(new File([data], filename, { type: "application/json" })); } } export async function uploadSettingsBackup(type: BackupType = "all", showToast = true): Promise { if (IS_DISCORD_DESKTOP) { const [file] = await DiscordNative.fileManager.openFiles({ filters: [ { name: "Equicord Settings Backup", extensions: ["json"] }, { name: "all", extensions: ["*"] } ] }); if (file) { try { await importSettings(new TextDecoder().decode(file.data), type); 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, type); if (showToast) toastSuccess(); } catch (err) { logger.error(err); if (showToast) toastFailure(err); } }; reader.readAsText(file); } }