160 lines
5.2 KiB
TypeScript
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);
|
|
}
|
|
}
|