Equicord/src/api/SettingsSync/offline.ts
2025-12-31 21:37:27 -05:00

160 lines
5.2 KiB
TypeScript

/*
* 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<void> {
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);
}
}